Ante Handler [ENG-545] (#16)

This commit is contained in:
David Terpay 2023-03-13 15:54:40 -04:00 committed by GitHub
parent 629a3b3edd
commit 1c96a6c3c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 731 additions and 5 deletions

1
go.mod
View File

@ -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
View File

@ -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=

View File

@ -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
View 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
View 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)
}
}
}

View 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
}

View File

@ -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 {

View 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)
}

View File

@ -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
}

View File

@ -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.