Ante Handler [ENG-545] (#16)
This commit is contained in:
parent
629a3b3edd
commit
1c96a6c3c2
1
go.mod
1
go.mod
@ -9,6 +9,7 @@ require (
|
||||
github.com/cosmos/cosmos-proto v1.0.0-beta.2
|
||||
github.com/cosmos/cosmos-sdk v0.47.0-rc3.0.20230308212038-818f6a047eeb
|
||||
github.com/cosmos/gogoproto v1.4.6
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/golang/protobuf v1.5.2
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0
|
||||
|
||||
8
go.sum
8
go.sum
@ -209,6 +209,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@ -489,6 +490,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/zondax/hid v0.9.1 h1:gQe66rtmyZ8VeGFcOpbuH3r7erYtNEAezCAYu8LdkJo=
|
||||
github.com/zondax/hid v0.9.1/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM=
|
||||
github.com/zondax/ledger-go v0.14.0 h1:dlMC7aO8Wss1CxBq2I96kZ69Nh1ligzbs8UWOtq/AsA=
|
||||
@ -552,6 +554,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -588,6 +591,7 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
@ -614,6 +618,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -662,8 +667,10 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -740,6 +747,7 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@ -20,13 +20,13 @@ message Params {
|
||||
// escrow_account_address is the address of the account that will hold the
|
||||
// funds for the auctions.
|
||||
string escrow_account_address = 2;
|
||||
// reserve_fee specifies a fee that the bidder must pay to enter the auction.
|
||||
// reserve_fee specifies the bid floor for the auction.
|
||||
repeated cosmos.base.v1beta1.Coin reserve_fee = 3 [
|
||||
(gogoproto.nullable) = false,
|
||||
(amino.dont_omitempty) = true,
|
||||
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
|
||||
];
|
||||
// min_buy_in_fee specifies the bid floor for the auction.
|
||||
// min_buy_in_fee specifies the fee that the bidder must pay to enter the auction.
|
||||
repeated cosmos.base.v1beta1.Coin min_buy_in_fee = 4 [
|
||||
(gogoproto.nullable) = false,
|
||||
(amino.dont_omitempty) = true,
|
||||
|
||||
55
x/auction/ante/ante.go
Normal file
55
x/auction/ante/ante.go
Normal file
@ -0,0 +1,55 @@
|
||||
package ante
|
||||
|
||||
import (
|
||||
"cosmossdk.io/errors"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/skip-mev/pob/mempool"
|
||||
"github.com/skip-mev/pob/x/auction/keeper"
|
||||
)
|
||||
|
||||
var _ sdk.AnteDecorator = AuctionDecorator{}
|
||||
|
||||
type AuctionDecorator struct {
|
||||
auctionKeeper keeper.Keeper
|
||||
txDecoder sdk.TxDecoder
|
||||
}
|
||||
|
||||
func NewAuctionDecorator(ak keeper.Keeper, txDecoder sdk.TxDecoder) AuctionDecorator {
|
||||
return AuctionDecorator{
|
||||
auctionKeeper: ak,
|
||||
txDecoder: txDecoder,
|
||||
}
|
||||
}
|
||||
|
||||
// AnteHandle validates that the auction bid is valid if one exists. If valid it will deduct the entrance fee from the
|
||||
// bidder's account.
|
||||
func (ad AuctionDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
|
||||
auctionMsg, err := mempool.GetMsgAuctionBidFromTx(tx)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
// Validate the auction bid if one exists.
|
||||
if auctionMsg != nil {
|
||||
bidder, err := sdk.AccAddressFromBech32(auctionMsg.Bidder)
|
||||
if err != nil {
|
||||
return ctx, errors.Wrapf(err, "invalid bidder address (%s)", auctionMsg.Bidder)
|
||||
}
|
||||
|
||||
transactions := make([]sdk.Tx, len(auctionMsg.Transactions))
|
||||
for i, tx := range auctionMsg.Transactions {
|
||||
decodedTx, err := ad.txDecoder(tx)
|
||||
if err != nil {
|
||||
return ctx, errors.Wrapf(err, "failed to decode transaction (%s)", tx)
|
||||
}
|
||||
|
||||
transactions[i] = decodedTx
|
||||
}
|
||||
|
||||
if err := ad.auctionKeeper.ValidateAuctionMsg(ctx, bidder, auctionMsg.Bid, transactions); err != nil {
|
||||
return ctx, errors.Wrap(err, "failed to validate auction bid")
|
||||
}
|
||||
}
|
||||
|
||||
return next(ctx, tx, simulate)
|
||||
}
|
||||
139
x/auction/keeper/auction.go
Normal file
139
x/auction/keeper/auction.go
Normal file
@ -0,0 +1,139 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// ValidateAuctionMsg validates that the MsgAuctionBid is valid. It checks that the bidder has sufficient funds to bid the
|
||||
// amount specified in the message, that the bundle size is not greater than the max bundle size, and that the bundle
|
||||
// transactions are valid.
|
||||
func (k Keeper) ValidateAuctionMsg(ctx sdk.Context, bidder sdk.AccAddress, bid sdk.Coins, transactions []sdk.Tx) error {
|
||||
// Validate the bundle size.
|
||||
maxBundleSize, err := k.GetMaxBundleSize(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if uint32(len(transactions)) > maxBundleSize {
|
||||
return fmt.Errorf("bundle size (%d) exceeds max bundle size (%d)", len(transactions), maxBundleSize)
|
||||
}
|
||||
|
||||
// Validate the bid amount.
|
||||
if err := k.ValidateAuctionBid(ctx, bidder, bid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate the bundle of transactions.
|
||||
if err := k.ValidateAuctionBundle(ctx, bidder, transactions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateAuctionBid validates that the bidder has sufficient funds to participate in the auction.
|
||||
func (k Keeper) ValidateAuctionBid(ctx sdk.Context, bidder sdk.AccAddress, bid sdk.Coins) error {
|
||||
// Get the bid floor.
|
||||
reserveFee, err := k.GetReserveFee(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bid.IsAllGTE(reserveFee) {
|
||||
return fmt.Errorf("bid amount (%s) is less than the reserve fee (%s)", bid, reserveFee)
|
||||
}
|
||||
|
||||
// Get the pay-to-play fee.
|
||||
minBuyInFee, err := k.GetMinBuyInFee(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure the bidder has enough funds to cover all the inclusion fees.
|
||||
minBalance := bid.Add(minBuyInFee...)
|
||||
balances := k.bankkeeper.GetAllBalances(ctx, bidder)
|
||||
if !balances.IsAllGTE(minBalance) {
|
||||
return fmt.Errorf("insufficient funds to bid %s (reserve fee + bid) with balance %s", minBalance, balances)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateAuctionBundle validates the ordering of the referenced transactions. Bundles are valid if
|
||||
// 1. all of the transactions are signed by the signer.
|
||||
// 2. some subset of contiguous transactions starting from the first tx are signed by the same signer, and all other tranasctions
|
||||
// are signed by the bidder.
|
||||
//
|
||||
// example:
|
||||
// 1. valid: [tx1, tx2, tx3] where tx1 is signed by the signer 1 and tx2 and tx3 are signed by the bidder.
|
||||
// 2. valid: [tx1, tx2, tx3, tx4] where tx1 - tx4 are signed by the bidder.
|
||||
// 3. invalid: [tx1, tx2, tx3] where tx1 and tx3 are signed by the bidder and tx2 is signed by some other signer. (possible sandwich attack)
|
||||
// 4. invalid: [tx1, tx2, tx3] where tx1 is signed by the bidder, and tx2 - tx3 are signed by some other signer. (possible front-running attack)
|
||||
func (k Keeper) ValidateAuctionBundle(ctx sdk.Context, bidder sdk.AccAddress, transactions []sdk.Tx) error {
|
||||
if len(transactions) <= 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// prevSigners is used to track whether the signers of the current transaction overlap.
|
||||
prevSigners, err := k.getTxSigners(transactions[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
seenBidder := prevSigners[bidder.String()]
|
||||
|
||||
// Check that all subsequent transactions are signed by either
|
||||
// 1. the same party as the first transaction
|
||||
// 2. the same party for some arbitrary number of txs and then are all remaining txs are signed by the bidder.
|
||||
for _, txbytes := range transactions[1:] {
|
||||
txSigners, err := k.getTxSigners(txbytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Filter the signers to only those that signed the current transaction.
|
||||
filterSigners(prevSigners, txSigners)
|
||||
|
||||
// If there are no overlapping signers from the previous tx and the bidder address has not been seen, then the bundle can still be valid
|
||||
// as long as all subsequent transactions are signed by the bidder.
|
||||
if len(prevSigners) == 0 {
|
||||
if seenBidder {
|
||||
return fmt.Errorf("bundle contains transactions signed by multiple parties. possible front-running or sandwich attack.")
|
||||
} else {
|
||||
seenBidder = true
|
||||
prevSigners = map[string]bool{bidder.String(): true}
|
||||
filterSigners(prevSigners, txSigners)
|
||||
|
||||
if len(prevSigners) == 0 {
|
||||
return fmt.Errorf("bundle contains transactions signed by multiple parties. possible front-running or sandwich attack.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTxSigners returns the signers of a transaction.
|
||||
func (k Keeper) getTxSigners(tx sdk.Tx) (map[string]bool, error) {
|
||||
signers := make(map[string]bool, 0)
|
||||
for _, msg := range tx.GetMsgs() {
|
||||
for _, signer := range msg.GetSigners() {
|
||||
// TODO: check for multi-sig accounts
|
||||
// https://github.com/skip-mev/pob/issues/14
|
||||
signers[signer.String()] = true
|
||||
}
|
||||
}
|
||||
|
||||
return signers, nil
|
||||
}
|
||||
|
||||
// filterSigners removes any signers from the currentSigners map that are not in the txSigners map.
|
||||
func filterSigners(currentSigners, txSigners map[string]bool) {
|
||||
for signer := range currentSigners {
|
||||
if _, ok := txSigners[signer]; !ok {
|
||||
delete(currentSigners, signer)
|
||||
}
|
||||
}
|
||||
}
|
||||
303
x/auction/keeper/auction_test.go
Normal file
303
x/auction/keeper/auction_test.go
Normal file
@ -0,0 +1,303 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/tx/signing"
|
||||
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
"github.com/skip-mev/pob/x/auction/keeper"
|
||||
auctiontypes "github.com/skip-mev/pob/x/auction/types"
|
||||
)
|
||||
|
||||
func (suite *IntegrationTestSuite) TestValidateAuctionMsg() {
|
||||
var (
|
||||
// Tx building variables
|
||||
accounts []Account = []Account{} // tracks the order of signers in the bundle
|
||||
balance sdk.Coins = sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(10000)))
|
||||
bid sdk.Coins = sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1000)))
|
||||
|
||||
// Auction params
|
||||
maxBundleSize uint32 = 10
|
||||
reserveFee sdk.Coins = sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1000)))
|
||||
minBuyInFee sdk.Coins = sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1000)))
|
||||
escrowAddress sdk.AccAddress = sdk.AccAddress([]byte("escrow"))
|
||||
)
|
||||
|
||||
rnd := rand.New(rand.NewSource(time.Now().Unix()))
|
||||
bidder := RandomAccounts(rnd, 1)[0]
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
malleate func()
|
||||
pass bool
|
||||
}{
|
||||
{
|
||||
"insufficient bid amount",
|
||||
func() {
|
||||
bid = sdk.NewCoins()
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"insufficient balance",
|
||||
func() {
|
||||
bid = sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1000)))
|
||||
balance = sdk.NewCoins()
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"bid amount equals the balance (not accounting for the reserve fee)",
|
||||
func() {
|
||||
balance = sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(2000)))
|
||||
bid = sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(2000)))
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"too many transactions in the bundle",
|
||||
func() {
|
||||
// reset the balance and bid to their original values
|
||||
bid = sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1000)))
|
||||
balance = sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(10000)))
|
||||
accounts = RandomAccounts(rnd, int(maxBundleSize+1))
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"frontrunning bundle",
|
||||
func() {
|
||||
randomAccount := RandomAccounts(rnd, 1)[0]
|
||||
accounts = []Account{bidder, randomAccount}
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"sandwiching bundle",
|
||||
func() {
|
||||
randomAccount := RandomAccounts(rnd, 1)[0]
|
||||
accounts = []Account{bidder, randomAccount, bidder}
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"valid bundle",
|
||||
func() {
|
||||
randomAccount := RandomAccounts(rnd, 1)[0]
|
||||
accounts = []Account{randomAccount, randomAccount, bidder, bidder, bidder}
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"valid bundle with only bidder txs",
|
||||
func() {
|
||||
accounts = []Account{bidder, bidder, bidder, bidder}
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"valid bundle with only random txs from single same user",
|
||||
func() {
|
||||
randomAccount := RandomAccounts(rnd, 1)[0]
|
||||
accounts = []Account{randomAccount, randomAccount, randomAccount, randomAccount}
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"invalid bundle with random accounts",
|
||||
func() {
|
||||
accounts = RandomAccounts(rnd, 2)
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
suite.Run(tc.name, func() {
|
||||
suite.SetupTest() // reset
|
||||
|
||||
tc.malleate()
|
||||
|
||||
// Set up the new auction keeper with mocks customized for this test case
|
||||
suite.bankKeeper.EXPECT().GetAllBalances(suite.ctx, bidder.Address).Return(balance).AnyTimes()
|
||||
suite.bankKeeper.EXPECT().SendCoins(suite.ctx, bidder.Address, escrowAddress, reserveFee).Return(nil).AnyTimes()
|
||||
|
||||
suite.auctionKeeper = keeper.NewKeeper(
|
||||
suite.encCfg.Codec,
|
||||
suite.key,
|
||||
suite.accountKeeper,
|
||||
suite.bankKeeper,
|
||||
suite.authorityAccount.String(),
|
||||
)
|
||||
params := auctiontypes.Params{
|
||||
MaxBundleSize: maxBundleSize,
|
||||
ReserveFee: reserveFee,
|
||||
MinBuyInFee: minBuyInFee,
|
||||
EscrowAccountAddress: escrowAddress.String(),
|
||||
}
|
||||
suite.auctionKeeper.SetParams(suite.ctx, params)
|
||||
|
||||
// Create the bundle of transactions ordered by accounts
|
||||
bundle := make([]sdk.Tx, 0)
|
||||
for _, acc := range accounts {
|
||||
tx, err := createRandomTx(suite.encCfg.TxConfig, acc, 0, 1)
|
||||
suite.Require().NoError(err)
|
||||
bundle = append(bundle, tx)
|
||||
}
|
||||
|
||||
err := suite.auctionKeeper.ValidateAuctionMsg(suite.ctx, bidder.Address, bid, bundle)
|
||||
if tc.pass {
|
||||
suite.Require().NoError(err)
|
||||
} else {
|
||||
suite.Require().Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *IntegrationTestSuite) TestValidateBundle() {
|
||||
var (
|
||||
// TODO: Update this to be multi-dimensional to test multi-sig
|
||||
// https://github.com/skip-mev/pob/issues/14
|
||||
accounts []Account // tracks the order of signers in the bundle
|
||||
)
|
||||
|
||||
rng := rand.New(rand.NewSource(time.Now().Unix()))
|
||||
bidder := RandomAccounts(rng, 1)[0]
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
malleate func()
|
||||
pass bool
|
||||
}{
|
||||
{
|
||||
"valid empty bundle",
|
||||
func() {
|
||||
accounts = make([]Account, 0)
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"valid single tx bundle",
|
||||
func() {
|
||||
accounts = []Account{bidder}
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"valid multi-tx bundle by same account",
|
||||
func() {
|
||||
accounts = []Account{bidder, bidder, bidder, bidder}
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"valid single-tx bundle by a different account",
|
||||
func() {
|
||||
randomAccount := RandomAccounts(rng, 1)[0]
|
||||
accounts = []Account{randomAccount}
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"valid multi-tx bundle by a different accounts",
|
||||
func() {
|
||||
randomAccount := RandomAccounts(rng, 1)[0]
|
||||
accounts = []Account{randomAccount, bidder}
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"invalid frontrunning bundle",
|
||||
func() {
|
||||
randomAccount := RandomAccounts(rng, 1)[0]
|
||||
accounts = []Account{bidder, randomAccount}
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid sandwiching bundle",
|
||||
func() {
|
||||
randomAccount := RandomAccounts(rng, 1)[0]
|
||||
accounts = []Account{bidder, randomAccount, bidder}
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid multi account bundle",
|
||||
func() {
|
||||
accounts = RandomAccounts(rng, 3)
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid multi account bundle without bidder",
|
||||
func() {
|
||||
randomAccount1 := RandomAccounts(rng, 1)[0]
|
||||
randomAccount2 := RandomAccounts(rng, 1)[0]
|
||||
accounts = []Account{randomAccount1, randomAccount2}
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
suite.Run(tc.name, func() {
|
||||
suite.SetupTest() // reset
|
||||
|
||||
// Malleate the test case
|
||||
tc.malleate()
|
||||
|
||||
// Create the bundle of transactions ordered by accounts
|
||||
bundle := make([]sdk.Tx, 0)
|
||||
for _, acc := range accounts {
|
||||
// Create a random tx
|
||||
tx, err := createRandomTx(suite.encCfg.TxConfig, acc, 0, 1)
|
||||
suite.Require().NoError(err)
|
||||
bundle = append(bundle, tx)
|
||||
}
|
||||
|
||||
// Validate the bundle
|
||||
err := suite.auctionKeeper.ValidateAuctionBundle(suite.ctx, bidder.Address, bundle)
|
||||
if tc.pass {
|
||||
suite.Require().NoError(err)
|
||||
} else {
|
||||
suite.Require().Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// createRandomTx creates a random transaction with a given account, nonce, and number of messages.
|
||||
func createRandomTx(txCfg client.TxConfig, account Account, nonce, numberMsgs uint64) (authsigning.Tx, error) {
|
||||
msgs := make([]sdk.Msg, numberMsgs)
|
||||
for i := 0; i < int(numberMsgs); i++ {
|
||||
msgs[i] = &banktypes.MsgSend{
|
||||
FromAddress: account.Address.String(),
|
||||
ToAddress: account.Address.String(),
|
||||
}
|
||||
}
|
||||
|
||||
txBuilder := txCfg.NewTxBuilder()
|
||||
if err := txBuilder.SetMsgs(msgs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sigV2 := signing.SignatureV2{
|
||||
PubKey: account.PrivKey.PubKey(),
|
||||
Data: &signing.SingleSignatureData{
|
||||
SignMode: txCfg.SignModeHandler().DefaultMode(),
|
||||
Signature: nil,
|
||||
},
|
||||
Sequence: nonce,
|
||||
}
|
||||
if err := txBuilder.SetSignatures(sigV2); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return txBuilder.GetTx(), nil
|
||||
}
|
||||
@ -120,7 +120,7 @@ func (k Keeper) GetReserveFee(ctx sdk.Context) (sdk.Coins, error) {
|
||||
return params.ReserveFee, nil
|
||||
}
|
||||
|
||||
// GetMinBuyInFee returns the bid floor for an auction.
|
||||
// GetMinBuyInFee returns the fee that the bidder must pay to enter the auction.
|
||||
func (k Keeper) GetMinBuyInFee(ctx sdk.Context) (sdk.Coins, error) {
|
||||
params, err := k.GetParams(ctx)
|
||||
if err != nil {
|
||||
|
||||
219
x/auction/keeper/keeper_test.go
Normal file
219
x/auction/keeper/keeper_test.go
Normal file
@ -0,0 +1,219 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
|
||||
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
|
||||
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
||||
"github.com/cosmos/cosmos-sdk/testutil"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/tx"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/skip-mev/pob/x/auction/ante"
|
||||
"github.com/skip-mev/pob/x/auction/keeper"
|
||||
"github.com/skip-mev/pob/x/auction/types"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type IntegrationTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
auctionKeeper keeper.Keeper
|
||||
bankKeeper *MockBankKeeper
|
||||
accountKeeper *MockAccountKeeper
|
||||
encCfg encodingConfig
|
||||
AuctionDecorator sdk.AnteDecorator
|
||||
ctx sdk.Context
|
||||
msgServer types.MsgServer
|
||||
key *storetypes.KVStoreKey
|
||||
authorityAccount sdk.AccAddress
|
||||
}
|
||||
|
||||
func TestKeeperTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(IntegrationTestSuite))
|
||||
}
|
||||
|
||||
func (suite *IntegrationTestSuite) SetupTest() {
|
||||
suite.encCfg = createTestEncodingConfig()
|
||||
suite.key = sdk.NewKVStoreKey(types.StoreKey)
|
||||
testCtx := testutil.DefaultContextWithDB(suite.T(), suite.key, sdk.NewTransientStoreKey("transient_test"))
|
||||
suite.ctx = testCtx.Ctx
|
||||
|
||||
// Auction Keeper setup
|
||||
ctrl := gomock.NewController(suite.T())
|
||||
suite.accountKeeper = NewMockAccountKeeper(ctrl)
|
||||
suite.accountKeeper.EXPECT().GetModuleAddress(types.ModuleName).Return(sdk.AccAddress{}).AnyTimes()
|
||||
suite.bankKeeper = NewMockBankKeeper(ctrl)
|
||||
suite.authorityAccount = sdk.AccAddress([]byte("authority"))
|
||||
suite.auctionKeeper = keeper.NewKeeper(suite.encCfg.Codec, suite.key, suite.accountKeeper, suite.bankKeeper, suite.authorityAccount.String())
|
||||
|
||||
err := suite.auctionKeeper.SetParams(suite.ctx, types.DefaultParams())
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.AuctionDecorator = ante.NewAuctionDecorator(suite.auctionKeeper, suite.encCfg.TxConfig.TxDecoder())
|
||||
suite.msgServer = keeper.NewMsgServerImpl(suite.auctionKeeper)
|
||||
}
|
||||
|
||||
type encodingConfig struct {
|
||||
InterfaceRegistry codectypes.InterfaceRegistry
|
||||
Codec codec.Codec
|
||||
TxConfig client.TxConfig
|
||||
Amino *codec.LegacyAmino
|
||||
}
|
||||
|
||||
func createTestEncodingConfig() encodingConfig {
|
||||
cdc := codec.NewLegacyAmino()
|
||||
interfaceRegistry := codectypes.NewInterfaceRegistry()
|
||||
|
||||
banktypes.RegisterInterfaces(interfaceRegistry)
|
||||
cryptocodec.RegisterInterfaces(interfaceRegistry)
|
||||
|
||||
codec := codec.NewProtoCodec(interfaceRegistry)
|
||||
|
||||
return encodingConfig{
|
||||
InterfaceRegistry: interfaceRegistry,
|
||||
Codec: codec,
|
||||
TxConfig: tx.NewTxConfig(codec, tx.DefaultSignModes),
|
||||
Amino: cdc,
|
||||
}
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
PrivKey cryptotypes.PrivKey
|
||||
PubKey cryptotypes.PubKey
|
||||
Address sdk.AccAddress
|
||||
ConsKey cryptotypes.PrivKey
|
||||
}
|
||||
|
||||
func (acc Account) Equals(acc2 Account) bool {
|
||||
return acc.Address.Equals(acc2.Address)
|
||||
}
|
||||
|
||||
// RandomAccounts returns a slice of n random accounts.
|
||||
func RandomAccounts(r *rand.Rand, n int) []Account {
|
||||
accs := make([]Account, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
pkSeed := make([]byte, 15)
|
||||
r.Read(pkSeed)
|
||||
|
||||
accs[i].PrivKey = secp256k1.GenPrivKeyFromSecret(pkSeed)
|
||||
accs[i].PubKey = accs[i].PrivKey.PubKey()
|
||||
accs[i].Address = sdk.AccAddress(accs[i].PubKey.Address())
|
||||
|
||||
accs[i].ConsKey = ed25519.GenPrivKeyFromSecret(pkSeed)
|
||||
}
|
||||
|
||||
return accs
|
||||
}
|
||||
|
||||
// MockAccountKeeper is a mock of AccountKeeper interface.
|
||||
type MockAccountKeeper struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockAccountKeeperMockRecorder
|
||||
}
|
||||
|
||||
// MockAccountKeeperMockRecorder is the mock recorder for MockAccountKeeper.
|
||||
type MockAccountKeeperMockRecorder struct {
|
||||
mock *MockAccountKeeper
|
||||
}
|
||||
|
||||
// NewMockAccountKeeper creates a new mock instance.
|
||||
func NewMockAccountKeeper(ctrl *gomock.Controller) *MockAccountKeeper {
|
||||
mock := &MockAccountKeeper{ctrl: ctrl}
|
||||
mock.recorder = &MockAccountKeeperMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockAccountKeeper) EXPECT() *MockAccountKeeperMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetModuleAddress mocks base method.
|
||||
func (m *MockAccountKeeper) GetModuleAddress(name string) sdk.AccAddress {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetModuleAddress", name)
|
||||
ret0, _ := ret[0].(sdk.AccAddress)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetModuleAddress indicates an expected call of GetModuleAddress.
|
||||
func (mr *MockAccountKeeperMockRecorder) GetModuleAddress(name interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetModuleAddress", reflect.TypeOf((*MockAccountKeeper)(nil).GetModuleAddress), name)
|
||||
}
|
||||
|
||||
// MockBankKeeper is a mock of BankKeeper interface.
|
||||
type MockBankKeeper struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockBankKeeperMockRecorder
|
||||
}
|
||||
|
||||
// MockBankKeeperMockRecorder is the mock recorder for MockBankKeeper.
|
||||
type MockBankKeeperMockRecorder struct {
|
||||
mock *MockBankKeeper
|
||||
}
|
||||
|
||||
// NewMockBankKeeper creates a new mock instance.
|
||||
func NewMockBankKeeper(ctrl *gomock.Controller) *MockBankKeeper {
|
||||
mock := &MockBankKeeper{ctrl: ctrl}
|
||||
mock.recorder = &MockBankKeeperMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockBankKeeper) EXPECT() *MockBankKeeperMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetAllBalances mocks base method.
|
||||
func (m *MockBankKeeper) GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAllBalances", ctx, addr)
|
||||
ret0 := ret[0].(sdk.Coins)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetAllBalances indicates an expected call of GetAllBalances.
|
||||
func (mr *MockBankKeeperMockRecorder) GetAllBalances(ctx, addr interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllBalances", reflect.TypeOf((*MockBankKeeper)(nil).GetAllBalances), ctx, addr)
|
||||
}
|
||||
|
||||
// SendCoins mocks base method.
|
||||
func (m *MockBankKeeper) SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "SendCoins", ctx, fromAddr, toAddr, amt)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendCoins indicates an expected call of SendCoins.
|
||||
func (mr *MockBankKeeperMockRecorder) SendCoins(ctx, fromAddr, toAddr, amt interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoins", reflect.TypeOf((*MockBankKeeper)(nil).SendCoins), ctx, fromAddr, toAddr, amt)
|
||||
}
|
||||
|
||||
// SendCoinsFromAccountToModule mocks base method.
|
||||
func (m *MockBankKeeper) SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SendCoinsFromAccountToModule", ctx, senderAddr, recipientModule, amt)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SendCoinsFromAccountToModule indicates an expected call of SendCoinsFromAccountToModule.
|
||||
func (mr *MockBankKeeperMockRecorder) SendCoinsFromAccountToModule(ctx, senderAddr, recipientModule, amt interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromAccountToModule", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromAccountToModule), ctx, senderAddr, recipientModule, amt)
|
||||
}
|
||||
@ -13,4 +13,5 @@ type AccountKeeper interface {
|
||||
type BankKeeper interface {
|
||||
SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error
|
||||
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
|
||||
GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
|
||||
}
|
||||
|
||||
@ -79,9 +79,9 @@ type Params struct {
|
||||
// escrow_account_address is the address of the account that will hold the
|
||||
// funds for the auctions.
|
||||
EscrowAccountAddress string `protobuf:"bytes,2,opt,name=escrow_account_address,json=escrowAccountAddress,proto3" json:"escrow_account_address,omitempty"`
|
||||
// reserve_fee specifies a fee that the bidder must pay to enter the auction.
|
||||
// reserve_fee specifies the bid floor for the auction.
|
||||
ReserveFee github_com_cosmos_cosmos_sdk_types.Coins `protobuf:"bytes,3,rep,name=reserve_fee,json=reserveFee,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.Coins" json:"reserve_fee"`
|
||||
// min_buy_in_fee specifies the bid floor for the auction.
|
||||
// min_buy_in_fee specifies the fee that the bidder must pay to enter the auction.
|
||||
MinBuyInFee github_com_cosmos_cosmos_sdk_types.Coins `protobuf:"bytes,4,rep,name=min_buy_in_fee,json=minBuyInFee,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.Coins" json:"min_buy_in_fee"`
|
||||
// min_bid_increment specifies the minimum amount that the next bid must be
|
||||
// greater than the previous bid.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user