diff --git a/lanes/mev/check_tx.go b/lanes/mev/check_tx.go index e5807f4..787343f 100644 --- a/lanes/mev/check_tx.go +++ b/lanes/mev/check_tx.go @@ -162,7 +162,10 @@ func (handler *CheckTxHandler) CheckTx() CheckTx { handler.baseApp.Logger().Info( "invalid bid tx", "err", err, - "tx", req.Tx, + "height", ctx.BlockHeight(), + "bid_height", bidInfo.Timeout, + "bidder", bidInfo.Bidder, + "bid", bidInfo.Bid, "removing tx from mempool", true, ) @@ -187,7 +190,10 @@ func (handler *CheckTxHandler) CheckTx() CheckTx { handler.baseApp.Logger().Info( "valid bid tx", - "tx", req.Tx, + "height", ctx.BlockHeight(), + "bid_height", bidInfo.Timeout, + "bidder", bidInfo.Bidder, + "bid", bidInfo.Bid, "inserting tx into mempool", true, ) diff --git a/tests/app/ante.go b/tests/app/ante.go index a48d09c..6a8f59b 100644 --- a/tests/app/ante.go +++ b/tests/app/ante.go @@ -11,7 +11,6 @@ import ( type BSDKHandlerOptions struct { BaseOptions ante.HandlerOptions - Mempool block.Mempool MEVLane auctionante.MEVLane TxDecoder sdk.TxDecoder TxEncoder sdk.TxEncoder @@ -55,7 +54,7 @@ func NewBSDKAnteHandler(options BSDKHandlerOptions) sdk.AnteHandler { ante.NewSigGasConsumeDecorator(options.BaseOptions.AccountKeeper, options.BaseOptions.SigGasConsumer), ante.NewSigVerificationDecorator(options.BaseOptions.AccountKeeper, options.BaseOptions.SignModeHandler), ante.NewIncrementSequenceDecorator(options.BaseOptions.AccountKeeper), - auctionante.NewAuctionDecorator(options.auctionkeeper, options.TxEncoder, options.MEVLane, options.Mempool), + auctionante.NewAuctionDecorator(options.auctionkeeper, options.TxEncoder, options.MEVLane), } return sdk.ChainAnteDecorators(anteDecorators...) diff --git a/tests/app/app.go b/tests/app/app.go index b32d1f5..d671907 100644 --- a/tests/app/app.go +++ b/tests/app/app.go @@ -328,7 +328,6 @@ func New( TxEncoder: app.txConfig.TxEncoder(), FreeLane: freeLane, MEVLane: mevLane, - Mempool: mempool, } anteHandler := NewBSDKAnteHandler(options) diff --git a/testutils/mocks.go b/testutils/mocks.go deleted file mode 100644 index 9aa36dc..0000000 --- a/testutils/mocks.go +++ /dev/null @@ -1,145 +0,0 @@ -package testutils - -import ( - "context" - "reflect" - - sdk "github.com/cosmos/cosmos-sdk/types" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - "github.com/golang/mock/gomock" -) - -type MockAccountKeeper struct { - ctrl *gomock.Controller - recorder *MockAccountKeeperMockRecorder -} - -type MockAccountKeeperMockRecorder struct { - mock *MockAccountKeeper -} - -func NewMockAccountKeeper(ctrl *gomock.Controller) *MockAccountKeeper { - mock := &MockAccountKeeper{ctrl: ctrl} - mock.recorder = &MockAccountKeeperMockRecorder{mock} - return mock -} - -func (m *MockAccountKeeper) EXPECT() *MockAccountKeeperMockRecorder { - return m.recorder -} - -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 -} - -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) -} - -type MockBankKeeper struct { - ctrl *gomock.Controller - recorder *MockBankKeeperMockRecorder -} - -type MockBankKeeperMockRecorder struct { - mock *MockBankKeeper -} - -func NewMockBankKeeper(ctrl *gomock.Controller) *MockBankKeeper { - mock := &MockBankKeeper{ctrl: ctrl} - mock.recorder = &MockBankKeeperMockRecorder{mock} - return mock -} - -func (m *MockBankKeeper) EXPECT() *MockBankKeeperMockRecorder { - return m.recorder -} - -func (m *MockBankKeeper) GetBalance(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBalance", ctx, addr, denom) - ret0 := ret[0].(sdk.Coin) - return ret0 -} - -func (mr *MockBankKeeperMockRecorder) GetBalance(ctx, addr, denom interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBalance", reflect.TypeOf((*MockBankKeeper)(nil).GetBalance), ctx, addr, denom) -} - -func (m *MockBankKeeper) SendCoins(ctx context.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 -} - -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) -} - -type MockDistributionKeeperRecorder struct { - mock *MockDistributionKeeper -} - -type MockDistributionKeeper struct { - ctrl *gomock.Controller - recorder *MockDistributionKeeperRecorder -} - -func NewMockDistributionKeeper(ctrl *gomock.Controller) *MockDistributionKeeper { - mock := &MockDistributionKeeper{ctrl: ctrl} - mock.recorder = &MockDistributionKeeperRecorder{mock} - return mock -} - -func (m *MockDistributionKeeper) EXPECT() *MockDistributionKeeperRecorder { - return m.recorder -} - -func (m *MockDistributionKeeper) GetPreviousProposerConsAddr(ctx context.Context) (sdk.ConsAddress, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPreviousProposerConsAddr", ctx) - ret0 := ret[0].(sdk.ConsAddress) - return ret0, nil -} - -func (mr *MockDistributionKeeperRecorder) GetPreviousProposerConsAddr(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPreviousProposerConsAddr", reflect.TypeOf((*MockDistributionKeeper)(nil).GetPreviousProposerConsAddr), ctx) -} - -type MockStakingKeeperRecorder struct { - mock *MockStakingKeeper -} - -type MockStakingKeeper struct { - ctrl *gomock.Controller - recorder *MockStakingKeeperRecorder -} - -func NewMockStakingKeeper(ctrl *gomock.Controller) *MockStakingKeeper { - mock := &MockStakingKeeper{ctrl: ctrl} - mock.recorder = &MockStakingKeeperRecorder{mock} - return mock -} - -func (m *MockStakingKeeper) EXPECT() *MockStakingKeeperRecorder { - return m.recorder -} - -func (m *MockStakingKeeper) GetValidatorByConsAddr(ctx context.Context, consAddr sdk.ConsAddress) (stakingtypes.Validator, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetValidatorByConsAddr", ctx, consAddr) - ret0 := ret[0].(stakingtypes.Validator) - return ret0, nil -} - -func (mr *MockStakingKeeperRecorder) GetValidatorByConsAddr(ctx, consAddr any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatorByConsAddr", reflect.TypeOf((*MockStakingKeeper)(nil).GetValidatorByConsAddr), ctx, consAddr) -} diff --git a/x/auction/ante/ante.go b/x/auction/ante/ante.go index 9526916..6b9fee0 100644 --- a/x/auction/ante/ante.go +++ b/x/auction/ante/ante.go @@ -2,46 +2,28 @@ package ante import ( "bytes" - "context" - "fmt" "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/skip-mev/block-sdk/x/auction/keeper" - "github.com/skip-mev/block-sdk/x/auction/types" ) var _ sdk.AnteDecorator = AuctionDecorator{} type ( - // MEVLane is an interface that defines the methods required to interact with the MEV - // lane. - MEVLane interface { - GetAuctionBidInfo(tx sdk.Tx) (*types.BidInfo, error) - GetTopAuctionTx(ctx context.Context) sdk.Tx - } - - // Mempool is an interface that defines the methods required to interact with the application-side mempool. - Mempool interface { - Contains(tx sdk.Tx) bool - } - // AuctionDecorator is an AnteDecorator that validates the auction bid and bundled transactions. AuctionDecorator struct { - auctionkeeper keeper.Keeper - txEncoder sdk.TxEncoder lane MEVLane - mempool Mempool + auctionkeeper AuctionKeeper + txEncoder sdk.TxEncoder } ) -func NewAuctionDecorator(ak keeper.Keeper, txEncoder sdk.TxEncoder, lane MEVLane, mempool Mempool) AuctionDecorator { +// NewAuctionDecorator returns a new AuctionDecorator. +func NewAuctionDecorator(ak AuctionKeeper, txEncoder sdk.TxEncoder, lane MEVLane) AuctionDecorator { return AuctionDecorator{ auctionkeeper: ak, txEncoder: txEncoder, lane: lane, - mempool: mempool, } } @@ -55,25 +37,14 @@ func (ad AuctionDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, // Validate the auction bid if one exists. if bidInfo != nil { - // If comet is re-checking a transaction, we only need to check if the transaction is in the application-side mempool. - if ctx.IsReCheckTx() { - if !ad.mempool.Contains(tx) { - return ctx, fmt.Errorf("transaction not found in application-side mempool") - } - } - // Auction transactions must have a timeout set to a valid block height. - if err := ad.ValidateTimeout(ctx, int64(bidInfo.Timeout)); err != nil { + if err := ValidateTimeout(ctx, int64(bidInfo.Timeout)); err != nil { return ctx, err } - // We only need to verify the auction bid relative to the local validator's mempool if the mode - // is checkTx or recheckTx. Otherwise, the ABCI handlers (VerifyVoteExtension, ExtendVoteExtension, etc.) - // will always compare the auction bid to the highest bidding transaction in the mempool leading to - // poor liveness guarantees. - // TODO(nikhil/david): refactor this logic (is this necessary?) + // Only compare the bid to the top bid if necessary. topBid := sdk.Coin{} - if ctx.IsCheckTx() || ctx.IsReCheckTx() { + if _, ok := nextHeightExecModes[ctx.ExecMode()]; ok { if topBidTx := ad.lane.GetTopAuctionTx(ctx); topBidTx != nil { topBidBz, err := ad.txEncoder(topBidTx) if err != nil { @@ -86,6 +57,8 @@ func (ad AuctionDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, } // Compare the bytes to see if the current transaction is the highest bidding transaction. + // If it is the same transaction, we do not need to compare the bids as the bid check will + // fail. if !bytes.Equal(topBidBz, currentTxBz) { topBidInfo, err := ad.lane.GetAuctionBidInfo(topBidTx) if err != nil { @@ -104,25 +77,3 @@ func (ad AuctionDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, return next(ctx, tx, simulate) } - -// ValidateTimeout validates that the timeout is greater than or equal to the expected block height -// the bid transaction will be executed in. -func (ad AuctionDecorator) ValidateTimeout(ctx sdk.Context, timeout int64) error { - currentBlockHeight := ctx.BlockHeight() - - // If the mode is CheckTx or ReCheckTx, we increment the current block height by one to - // account for the fact that the transaction will be executed in the next block. - if ctx.IsCheckTx() || ctx.IsReCheckTx() { - currentBlockHeight++ - } - - if timeout < currentBlockHeight { - return fmt.Errorf( - "timeout height cannot be less than the current block height (timeout: %d, current block height: %d)", - timeout, - currentBlockHeight, - ) - } - - return nil -} diff --git a/x/auction/ante/ante_test.go b/x/auction/ante/ante_test.go deleted file mode 100644 index 0f07afb..0000000 --- a/x/auction/ante/ante_test.go +++ /dev/null @@ -1,311 +0,0 @@ -package ante_test - -import ( - "math/rand" - "testing" - "time" - - "cosmossdk.io/log" - "cosmossdk.io/math" - storetypes "cosmossdk.io/store/types" - "github.com/cosmos/cosmos-sdk/testutil" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/suite" - - signer_extraction "github.com/skip-mev/block-sdk/adapters/signer_extraction_adapter" - "github.com/skip-mev/block-sdk/block" - "github.com/skip-mev/block-sdk/block/base" - defaultlane "github.com/skip-mev/block-sdk/lanes/base" - "github.com/skip-mev/block-sdk/lanes/mev" - testutils "github.com/skip-mev/block-sdk/testutils" - "github.com/skip-mev/block-sdk/x/auction/ante" - "github.com/skip-mev/block-sdk/x/auction/keeper" - auctiontypes "github.com/skip-mev/block-sdk/x/auction/types" -) - -type AnteTestSuite struct { - suite.Suite - ctx sdk.Context - - encodingConfig testutils.EncodingConfig - random *rand.Rand - - // auction setup - auctionkeeper keeper.Keeper - bankKeeper *testutils.MockBankKeeper - accountKeeper *testutils.MockAccountKeeper - distrKeeper *testutils.MockDistributionKeeper - stakingKeeper *testutils.MockStakingKeeper - auctionDecorator ante.AuctionDecorator - key *storetypes.KVStoreKey - authorityAccount sdk.AccAddress - - // mempool and lane set up - mempool block.Mempool - mevLane *mev.MEVLane - baseLane *defaultlane.DefaultLane - lanes []block.Lane - - // Account set up - balance sdk.Coin -} - -func TestAnteTestSuite(t *testing.T) { - suite.Run(t, new(AnteTestSuite)) -} - -func (suite *AnteTestSuite) SetupTest() { - // General config - suite.encodingConfig = testutils.CreateTestEncodingConfig() - suite.random = rand.New(rand.NewSource(time.Now().Unix())) - suite.key = storetypes.NewKVStoreKey(auctiontypes.StoreKey) - testCtx := testutil.DefaultContextWithDB(suite.T(), suite.key, storetypes.NewTransientStoreKey("transient_test")) - suite.ctx = testCtx.Ctx.WithIsCheckTx(true) - - // Keepers set up - ctrl := gomock.NewController(suite.T()) - suite.accountKeeper = testutils.NewMockAccountKeeper(ctrl) - suite.accountKeeper.EXPECT().GetModuleAddress(auctiontypes.ModuleName).Return(sdk.AccAddress{}).AnyTimes() - suite.bankKeeper = testutils.NewMockBankKeeper(ctrl) - suite.distrKeeper = testutils.NewMockDistributionKeeper(ctrl) - suite.stakingKeeper = testutils.NewMockStakingKeeper(ctrl) - suite.authorityAccount = sdk.AccAddress([]byte("authority")) - suite.auctionkeeper = keeper.NewKeeper( - suite.encodingConfig.Codec, - suite.key, - suite.accountKeeper, - suite.bankKeeper, - suite.distrKeeper, - suite.stakingKeeper, - suite.authorityAccount.String(), - ) - err := suite.auctionkeeper.SetParams(suite.ctx, auctiontypes.DefaultParams()) - suite.Require().NoError(err) - - // Lanes configuration - // - // TOB lane set up - mevConfig := base.LaneConfig{ - Logger: suite.ctx.Logger(), - TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), - TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), - SignerExtractor: signer_extraction.NewDefaultAdapter(), - AnteHandler: suite.anteHandler, - MaxBlockSpace: math.LegacyZeroDec(), - } - suite.mevLane = mev.NewMEVLane( - mevConfig, - mev.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder(), signer_extraction.NewDefaultAdapter()), - ) - - // Base lane set up - baseConfig := base.LaneConfig{ - Logger: suite.ctx.Logger(), - TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), - TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), - AnteHandler: suite.anteHandler, - SignerExtractor: signer_extraction.NewDefaultAdapter(), - MaxBlockSpace: math.LegacyZeroDec(), - IgnoreList: []block.Lane{suite.mevLane}, - } - suite.baseLane = defaultlane.NewDefaultLane(baseConfig) - - // Mempool set up - suite.lanes = []block.Lane{suite.mevLane, suite.baseLane} - suite.mempool = block.NewLanedMempool(log.NewTestLogger(suite.T()), true, suite.lanes...) -} - -func (suite *AnteTestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, _ bool) (sdk.Context, error) { - suite.bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), suite.balance.Denom).AnyTimes().Return(suite.balance) - - next := func(ctx sdk.Context, tx sdk.Tx, _ bool) (sdk.Context, error) { - return ctx, nil - } - - return suite.auctionDecorator.AnteHandle(ctx, tx, false, next) -} - -func (suite *AnteTestSuite) TestAnteHandler() { - var ( - // Bid set up - bidder = testutils.RandomAccounts(suite.random, 1)[0] - bid = sdk.NewCoin("stake", math.NewInt(1000)) - balance = sdk.NewCoin("stake", math.NewInt(10000)) - signers = []testutils.Account{bidder} - - // Top bidding auction tx set up - topBidder = testutils.RandomAccounts(suite.random, 1)[0] - topBid = sdk.NewCoin("stake", math.NewInt(100)) - insertTopBid = true - timeout = uint64(1000) - - // Auction setup - maxBundleSize uint32 = 5 - reserveFee = sdk.NewCoin("stake", math.NewInt(100)) - minBidIncrement = sdk.NewCoin("stake", math.NewInt(100)) - frontRunningProtection = true - ) - - cases := []struct { - name string - malleate func() - pass bool - }{ - { - "empty mempool, valid bid", - func() { - insertTopBid = false - }, - true, - }, - { - "smaller bid than winning bid, invalid auction tx", - func() { - insertTopBid = true - topBid = sdk.NewCoin("stake", math.NewInt(100000)) - }, - false, - }, - { - "bidder has insufficient balance, invalid auction tx", - func() { - insertTopBid = false - balance = sdk.NewCoin("stake", math.NewInt(10)) - }, - false, - }, - { - "bid is smaller than reserve fee, invalid auction tx", - func() { - balance = sdk.NewCoin("stake", math.NewInt(10000)) - bid = sdk.NewCoin("stake", math.NewInt(101)) - reserveFee = sdk.NewCoin("stake", math.NewInt(1000)) - }, - false, - }, - { - "valid mev bid tx", - func() { - balance = sdk.NewCoin("stake", math.NewInt(10000)) - bid = sdk.NewCoin("stake", math.NewInt(1000)) - reserveFee = sdk.NewCoin("stake", math.NewInt(100)) - }, - true, - }, - { - "invalid mev bid tx with no timeout", - func() { - timeout = 0 - }, - false, - }, - { - "mev tx is the top bidding tx", - func() { - timeout = 1000 - balance = sdk.NewCoin("stake", math.NewInt(10000)) - bid = sdk.NewCoin("stake", math.NewInt(1000)) - reserveFee = sdk.NewCoin("stake", math.NewInt(100)) - - insertTopBid = true - topBidder = bidder - topBid = bid - signers = []testutils.Account{} - }, - true, - }, - { - "invalid frontrunning mev bid tx", - func() { - randomAccount := testutils.RandomAccounts(suite.random, 2) - bidder := randomAccount[0] - otherUser := randomAccount[1] - insertTopBid = false - - signers = []testutils.Account{bidder, otherUser} - }, - false, - }, - { - "valid frontrunning mev bid tx", - func() { - randomAccount := testutils.RandomAccounts(suite.random, 2) - bidder := randomAccount[0] - otherUser := randomAccount[1] - - signers = []testutils.Account{bidder, otherUser} - frontRunningProtection = false - }, - true, - }, - { - "invalid sandwiching mev bid tx", - func() { - randomAccount := testutils.RandomAccounts(suite.random, 2) - bidder := randomAccount[0] - otherUser := randomAccount[1] - - signers = []testutils.Account{bidder, otherUser, bidder} - frontRunningProtection = true - }, - false, - }, - { - "invalid mev bid tx with many signers", - func() { - signers = testutils.RandomAccounts(suite.random, 10) - frontRunningProtection = true - }, - false, - }, - } - - for _, tc := range cases { - suite.Run(tc.name, func() { - suite.SetupTest() - tc.malleate() - - suite.ctx = suite.ctx.WithBlockHeight(1) - - // Set the mev params - err := suite.auctionkeeper.SetParams(suite.ctx, auctiontypes.Params{ - MaxBundleSize: maxBundleSize, - ReserveFee: reserveFee, - MinBidIncrement: minBidIncrement, - FrontRunningProtection: frontRunningProtection, - }) - suite.Require().NoError(err) - - // Insert the top bid into the mempool - if insertTopBid { - topAuctionTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, topBidder, topBid, 0, timeout, []testutils.Account{}) - suite.Require().NoError(err) - - distribution := suite.mempool.GetTxDistribution() - suite.Require().Equal(0, distribution[mev.LaneName]) - suite.Require().Equal(0, distribution[defaultlane.LaneName]) - - suite.Require().NoError(suite.mempool.Insert(suite.ctx, topAuctionTx)) - - distribution = suite.mempool.GetTxDistribution() - suite.Require().Equal(1, distribution[mev.LaneName]) - suite.Require().Equal(0, distribution[defaultlane.LaneName]) - } - - // Create the actual mev tx and insert into the mempool - mevTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 0, timeout, signers) - suite.Require().NoError(err) - - // Execute the ante handler - suite.balance = balance - suite.auctionDecorator = ante.NewAuctionDecorator(suite.auctionkeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mevLane, suite.mempool) - _, err = suite.anteHandler(suite.ctx, mevTx, false) - if tc.pass { - suite.Require().NoError(err) - } else { - suite.Require().Error(err) - } - }) - } -} diff --git a/x/auction/ante/utils.go b/x/auction/ante/utils.go new file mode 100644 index 0000000..b13e780 --- /dev/null +++ b/x/auction/ante/utils.go @@ -0,0 +1,53 @@ +package ante + +import ( + "context" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/block-sdk/x/auction/types" +) + +// nextHeightExecModes is the subset of execution modes that need special treatment. In +// particular, these modes are used to signal that the transaction is being executed in the +// next block. This is used to determine if the current transaction should be compared +// against the top bid and what the expected timeout height should be. +var nextHeightExecModes = map[sdk.ExecMode]struct{}{ + sdk.ExecModeCheck: {}, + sdk.ExecModeReCheck: {}, + sdk.ExecModeSimulate: {}, +} + +// MEVLane is an interface that defines the methods required to interact with the MEV +// lane. +type MEVLane interface { + GetAuctionBidInfo(tx sdk.Tx) (*types.BidInfo, error) + GetTopAuctionTx(ctx context.Context) sdk.Tx +} + +// AuctionKeeper is an interface that defines the methods required to interact with the +// auction keeper. +type AuctionKeeper interface { + ValidateBidInfo(ctx sdk.Context, highestBid sdk.Coin, bidInfo *types.BidInfo) error +} + +// ValidateTimeout validates that the timeout is greater than or equal to the expected block height +// the bid transaction will be executed in. +func ValidateTimeout(ctx sdk.Context, timeout int64) error { + // Every transaction must have a timeout height greater than or equal to the height at which + // the bid transaction will be executed. + height := ctx.BlockHeight() + if _, ok := nextHeightExecModes[ctx.ExecMode()]; ok { + height++ + } + + if height > timeout { + return fmt.Errorf( + "timeout height cannot be less than the current block height (timeout: %d, current block height: %d)", + timeout, + height, + ) + } + + return nil +} diff --git a/x/auction/keeper/auction.go b/x/auction/keeper/auction.go index f29d585..6a70a16 100644 --- a/x/auction/keeper/auction.go +++ b/x/auction/keeper/auction.go @@ -86,10 +86,49 @@ func (k Keeper) ValidateAuctionBid(ctx sdk.Context, bidder sdk.AccAddress, bid, } } - // ensure the bidder has enough funds to cover all the inclusion fees - balances := k.bankKeeper.GetBalance(ctx, bidder, bid.Denom) - if !balances.IsGTE(bid) { - return fmt.Errorf("insufficient funds to bid %s with balance %s", bid, balances) + // Extract the bid from the bidder. + return k.ExtractBid(ctx, bidder, bid) +} + +// ExtractBid extracts the bid amount from the transaction. +func (k Keeper) ExtractBid(ctx sdk.Context, bidder sdk.AccAddress, bid sdk.Coin) error { + params, err := k.GetParams(ctx) + if err != nil { + return err + } + + escrowAddress := sdk.AccAddress(params.EscrowAccountAddress) + + var proposerReward sdk.Coins + if params.ProposerFee.IsZero() { + // send the entire bid to the escrow account when no proposer fee is set + if err := k.bankKeeper.SendCoins(ctx, bidder, escrowAddress, sdk.NewCoins(bid)); err != nil { + return err + } + } else { + rewardsAddress, err := k.rewardsAddressProvider.GetRewardsAddress(ctx) + if err != nil { + // In the case where the rewards address provider returns an error, the + // escrow account will receive the entire bid. + rewardsAddress = escrowAddress + } + + // determine the amount of the bid that goes to the (previous) proposer + bid := sdk.NewDecCoinsFromCoins(bid) + proposerReward, _ = bid.MulDecTruncate(params.ProposerFee).TruncateDecimal() + + if err := k.bankKeeper.SendCoins(ctx, bidder, rewardsAddress, proposerReward); err != nil { + return err + } + + // Determine the amount of the remaining bid that goes to the escrow account. + // If a decimal remainder exists, it'll stay with the bidding account. + escrowTotal := bid.Sub(sdk.NewDecCoinsFromCoins(proposerReward...)) + escrowReward, _ := escrowTotal.TruncateDecimal() + + if err := k.bankKeeper.SendCoins(ctx, bidder, escrowAddress, escrowReward); err != nil { + return err + } } return nil diff --git a/x/auction/keeper/auction_test.go b/x/auction/keeper/auction_test.go index b100401..069fae6 100644 --- a/x/auction/keeper/auction_test.go +++ b/x/auction/keeper/auction_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "fmt" "math/rand" "time" @@ -10,215 +11,114 @@ import ( testutils "github.com/skip-mev/block-sdk/testutils" "github.com/skip-mev/block-sdk/x/auction/keeper" "github.com/skip-mev/block-sdk/x/auction/types" + "github.com/skip-mev/block-sdk/x/auction/types/mocks" + mock "github.com/stretchr/testify/mock" ) -func (suite *KeeperTestSuite) TestValidateBidInfo() { - var ( - // Tx building variables - accounts = []testutils.Account{} // tracks the order of signers in the bundle - balance = sdk.NewCoin("stake", math.NewInt(10000)) - bid = sdk.NewCoin("stake", math.NewInt(1000)) +func (s *KeeperTestSuite) TestValidateAuctionBid() { + rng := rand.New(rand.NewSource(time.Now().Unix())) + bidder := testutils.RandomAccounts(rng, 1)[0].Address + bankSendErr := fmt.Errorf("bank send error") - // Auction params - maxBundleSize uint32 = 10 - reserveFee = sdk.NewCoin("stake", math.NewInt(1000)) - minBidIncrement = sdk.NewCoin("stake", math.NewInt(1000)) - escrowAddress = sdk.AccAddress([]byte("escrow")) - frontRunningProtection = true - - // mempool variables - highestBid = sdk.Coin{} - ) - - rnd := rand.New(rand.NewSource(time.Now().Unix())) - bidder := testutils.RandomAccounts(rnd, 1)[0] - - cases := []struct { - name string - malleate func() - pass bool - }{ - { - "insufficient bid amount", - func() { - bid = sdk.Coin{} - }, - false, - }, - { - "insufficient balance", - func() { - bid = sdk.NewCoin("stake", math.NewInt(1000)) - balance = sdk.NewCoin("stake", math.NewInt(100)) - }, - false, - }, - { - "too many transactions in the bundle", - func() { - // reset the balance and bid to their original values - bid = sdk.NewCoin("stake", math.NewInt(1000)) - balance = sdk.NewCoin("stake", math.NewInt(10000)) - accounts = testutils.RandomAccounts(rnd, int(maxBundleSize+1)) - }, - false, - }, - { - "frontrunning bundle", - func() { - randomAccount := testutils.RandomAccounts(rnd, 1)[0] - accounts = []testutils.Account{bidder, randomAccount} - }, - false, - }, - { - "sandwiching bundle", - func() { - randomAccount := testutils.RandomAccounts(rnd, 1)[0] - accounts = []testutils.Account{bidder, randomAccount, bidder} - }, - false, - }, - { - "valid bundle", - func() { - randomAccount := testutils.RandomAccounts(rnd, 1)[0] - accounts = []testutils.Account{randomAccount, randomAccount, bidder, bidder, bidder} - }, - true, - }, - { - "valid bundle with only bidder txs", - func() { - accounts = []testutils.Account{bidder, bidder, bidder, bidder} - }, - true, - }, - { - "valid bundle with only random txs from single same user", - func() { - randomAccount := testutils.RandomAccounts(rnd, 1)[0] - accounts = []testutils.Account{randomAccount, randomAccount, randomAccount, randomAccount} - }, - true, - }, - { - "invalid bundle with random accounts", - func() { - accounts = testutils.RandomAccounts(rnd, 2) - }, - false, - }, - { - "disabled front-running protection", - func() { - accounts = testutils.RandomAccounts(rnd, 10) - frontRunningProtection = false - }, - true, - }, - { - "invalid bundle that does not outbid the highest bid", - func() { - accounts = []testutils.Account{bidder, bidder, bidder} - highestBid = sdk.NewCoin("stake", math.NewInt(500)) - bid = sdk.NewCoin("stake", math.NewInt(500)) - }, - false, - }, - { - "valid bundle that outbids the highest bid", - func() { - highestBid = sdk.NewCoin("stake", math.NewInt(500)) - bid = sdk.NewCoin("stake", math.NewInt(1500)) - }, - true, - }, - { - "attempting to bid with a different denom", - func() { - highestBid = sdk.NewCoin("stake", math.NewInt(500)) - bid = sdk.NewCoin("stake2", math.NewInt(1500)) - }, - false, - }, - { - "min bid increment is different from bid denom", // THIS SHOULD NEVER HAPPEN - func() { - highestBid = sdk.NewCoin("stake", math.NewInt(500)) - bid = sdk.NewCoin("stake", math.NewInt(1500)) - minBidIncrement = sdk.NewCoin("stake2", math.NewInt(1000)) - }, - false, - }, + params := types.Params{ + ReserveFee: sdk.NewCoin("stake", math.NewInt(1000)), + EscrowAccountAddress: sdk.AccAddress([]byte("escrow")), + MinBidIncrement: sdk.NewCoin("stake", math.NewInt(1000)), + ProposerFee: math.LegacyZeroDec(), } + s.Require().NoError(s.auctionkeeper.SetParams(s.ctx, params)) - for _, tc := range cases { - suite.Run(tc.name, func() { - suite.SetupTest() // reset + s.Run("nil bid", func() { + highestBid := sdk.NewCoin("stake", math.NewInt(1000)) + s.Require().Error(s.auctionkeeper.ValidateAuctionBid(s.ctx, bidder, sdk.Coin{}, highestBid)) + }) - tc.malleate() + s.Run("reserve fee and bid denom mismatch", func() { + highestBid := sdk.NewCoin("stake", math.NewInt(1000)) + bid := sdk.NewCoin("stake2", math.NewInt(1000)) + s.Require().Error(s.auctionkeeper.ValidateAuctionBid(s.ctx, bidder, bid, highestBid)) + }) - // Set up the new auction keeper with mocks customized for this test case - suite.bankKeeper.EXPECT().GetBalance(suite.ctx, bidder.Address, minBidIncrement.Denom).Return(balance).AnyTimes() - suite.bankKeeper.EXPECT().SendCoins(suite.ctx, bidder.Address, escrowAddress, reserveFee).Return(nil).AnyTimes() + s.Run("bid less than reserve fee", func() { + highestBid := sdk.NewCoin("stake", math.NewInt(1000)) + bid := sdk.NewCoin("stake", math.NewInt(500)) + s.Require().Error(s.auctionkeeper.ValidateAuctionBid(s.ctx, bidder, bid, highestBid)) + }) - suite.auctionkeeper = keeper.NewKeeper( - suite.encCfg.Codec, - suite.key, - suite.accountKeeper, - suite.bankKeeper, - suite.distrKeeper, - suite.stakingKeeper, - suite.authorityAccount.String(), - ) - params := types.Params{ - MaxBundleSize: maxBundleSize, - ReserveFee: reserveFee, - EscrowAccountAddress: escrowAddress, - FrontRunningProtection: frontRunningProtection, - MinBidIncrement: minBidIncrement, - } - suite.auctionkeeper.SetParams(suite.ctx, params) + s.Run("bid less than highest bid + min bid increment", func() { + highestBid := sdk.NewCoin("stake", math.NewInt(1000)) + bid := sdk.NewCoin("stake", math.NewInt(1500)) + s.Require().Error(s.auctionkeeper.ValidateAuctionBid(s.ctx, bidder, bid, highestBid)) + }) - // Create the bundle of transactions ordered by accounts - bundle := make([][]byte, 0) - for _, acc := range accounts { - tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, acc, 0, 1, 100, 0) - suite.Require().NoError(err) + s.Run("valid bid", func() { + highestBid := sdk.Coin{} + bid := sdk.NewCoin("stake", math.NewInt(1500)) - txBz, err := suite.encCfg.TxConfig.TxEncoder()(tx) - suite.Require().NoError(err) - bundle = append(bundle, txBz) - } + s.bankKeeper.On("SendCoins", mock.Anything, mock.Anything, mock.Anything, sdk.NewCoins(bid)).Return(nil) - signers := make([]map[string]struct{}, len(accounts)) - for index, acc := range accounts { - txSigners := map[string]struct{}{ - acc.Address.String(): {}, - } + err := s.auctionkeeper.ValidateAuctionBid(s.ctx, bidder, bid, highestBid) + s.Require().NoError(err) + }) - signers[index] = txSigners - } + s.Run("insufficient funds", func() { + highestBid := sdk.Coin{} + bid := sdk.NewCoin("stake", math.NewInt(1500)) - bidInfo := &types.BidInfo{ - Bidder: bidder.Address, - Bid: bid, - Transactions: bundle, - Signers: signers, - } + s.bankKeeper = mocks.NewBankKeeper(s.T()) + s.auctionkeeper = keeper.NewKeeper( + s.encCfg.Codec, + s.key, + s.accountKeeper, + s.bankKeeper, + s.distrKeeper, + s.stakingKeeper, + s.authorityAccount.String(), + ) - err := suite.auctionkeeper.ValidateBidInfo(suite.ctx, highestBid, bidInfo) - if tc.pass { - suite.Require().NoError(err) - } else { - suite.Require().Error(err) - } - }) - } + s.bankKeeper.On("SendCoins", mock.Anything, mock.Anything, mock.Anything, sdk.NewCoins(bid)).Return(bankSendErr) + + err := s.auctionkeeper.ValidateAuctionBid(s.ctx, bidder, bid, highestBid) + s.Require().Error(err) + }) + + s.Run("valid bid with proposer split", func() { + highestBid := sdk.Coin{} + bid := sdk.NewCoin("stake", math.NewInt(1000)) + + s.bankKeeper = mocks.NewBankKeeper(s.T()) + rewardsProvider := mocks.NewRewardsAddressProvider(s.T()) + rewardsAddr := sdk.AccAddress([]byte("rewards")) + rewardsProvider.On("GetRewardsAddress", mock.Anything).Return(rewardsAddr, nil) + + s.auctionkeeper = keeper.NewKeeperWithRewardsAddressProvider( + s.encCfg.Codec, + s.key, + s.accountKeeper, + s.bankKeeper, + rewardsProvider, + s.authorityAccount.String(), + ) + + params := types.Params{ + ProposerFee: math.LegacyMustNewDecFromStr("0.1"), + ReserveFee: sdk.NewCoin("stake", math.NewInt(1000)), + EscrowAccountAddress: sdk.AccAddress([]byte("escrow")), + MinBidIncrement: sdk.NewCoin("stake", math.NewInt(1000)), + } + s.Require().NoError(s.auctionkeeper.SetParams(s.ctx, params)) + + proposalSplit := sdk.NewCoin("stake", math.NewInt(100)) + escrowSplit := sdk.NewCoin("stake", math.NewInt(900)) + s.bankKeeper.On("SendCoins", mock.Anything, mock.Anything, mock.Anything, sdk.NewCoins(proposalSplit)).Return(nil) + s.bankKeeper.On("SendCoins", mock.Anything, mock.Anything, mock.Anything, sdk.NewCoins(escrowSplit)).Return(nil) + + err := s.auctionkeeper.ValidateAuctionBid(s.ctx, bidder, bid, highestBid) + s.Require().NoError(err) + }) } -func (suite *KeeperTestSuite) TestValidateBundle() { +func (s *KeeperTestSuite) TestValidateBundle() { var accounts []testutils.Account // tracks the order of signers in the bundle rng := rand.New(rand.NewSource(time.Now().Unix())) @@ -301,8 +201,8 @@ func (suite *KeeperTestSuite) TestValidateBundle() { } for _, tc := range cases { - suite.Run(tc.name, func() { - suite.SetupTest() // reset + s.Run(tc.name, func() { + s.SetupTest() // reset // Malleate the test case tc.malleate() @@ -317,11 +217,11 @@ func (suite *KeeperTestSuite) TestValidateBundle() { } // Validate the bundle - err := suite.auctionkeeper.ValidateAuctionBundle(bidder.Address, signers) + err := s.auctionkeeper.ValidateAuctionBundle(bidder.Address, signers) if tc.pass { - suite.Require().NoError(err) + s.Require().NoError(err) } else { - suite.Require().Error(err) + s.Require().Error(err) } }) } diff --git a/x/auction/keeper/keeper_test.go b/x/auction/keeper/keeper_test.go index 075001a..f77fc70 100644 --- a/x/auction/keeper/keeper_test.go +++ b/x/auction/keeper/keeper_test.go @@ -6,11 +6,11 @@ import ( storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/golang/mock/gomock" testutils "github.com/skip-mev/block-sdk/testutils" "github.com/skip-mev/block-sdk/x/auction/keeper" "github.com/skip-mev/block-sdk/x/auction/types" + "github.com/skip-mev/block-sdk/x/auction/types/mocks" "github.com/stretchr/testify/suite" ) @@ -19,10 +19,10 @@ type KeeperTestSuite struct { suite.Suite auctionkeeper keeper.Keeper - bankKeeper *testutils.MockBankKeeper - accountKeeper *testutils.MockAccountKeeper - distrKeeper *testutils.MockDistributionKeeper - stakingKeeper *testutils.MockStakingKeeper + bankKeeper *mocks.BankKeeper + accountKeeper *mocks.AccountKeeper + distrKeeper *mocks.DistributionKeeper + stakingKeeper *mocks.StakingKeeper encCfg testutils.EncodingConfig ctx sdk.Context msgServer types.MsgServer @@ -34,33 +34,31 @@ func TestKeeperTestSuite(t *testing.T) { suite.Run(t, new(KeeperTestSuite)) } -func (suite *KeeperTestSuite) SetupTest() { - suite.encCfg = testutils.CreateTestEncodingConfig() - suite.key = storetypes.NewKVStoreKey(types.StoreKey) - testCtx := testutil.DefaultContextWithDB(suite.T(), suite.key, storetypes.NewTransientStoreKey("transient_test")) - suite.ctx = testCtx.Ctx +func (s *KeeperTestSuite) SetupTest() { + s.encCfg = testutils.CreateTestEncodingConfig() + s.key = storetypes.NewKVStoreKey(types.StoreKey) + testCtx := testutil.DefaultContextWithDB(s.T(), s.key, storetypes.NewTransientStoreKey("transient_test")) + s.ctx = testCtx.Ctx - ctrl := gomock.NewController(suite.T()) + s.accountKeeper = mocks.NewAccountKeeper(s.T()) + s.accountKeeper.On("GetModuleAddress", types.ModuleName).Return(sdk.AccAddress{}).Maybe() - suite.accountKeeper = testutils.NewMockAccountKeeper(ctrl) - suite.accountKeeper.EXPECT().GetModuleAddress(types.ModuleName).Return(sdk.AccAddress{}).AnyTimes() - - suite.bankKeeper = testutils.NewMockBankKeeper(ctrl) - suite.distrKeeper = testutils.NewMockDistributionKeeper(ctrl) - suite.stakingKeeper = testutils.NewMockStakingKeeper(ctrl) - suite.authorityAccount = sdk.AccAddress([]byte("authority")) - suite.auctionkeeper = keeper.NewKeeper( - suite.encCfg.Codec, - suite.key, - suite.accountKeeper, - suite.bankKeeper, - suite.distrKeeper, - suite.stakingKeeper, - suite.authorityAccount.String(), + s.bankKeeper = mocks.NewBankKeeper(s.T()) + s.distrKeeper = mocks.NewDistributionKeeper(s.T()) + s.stakingKeeper = mocks.NewStakingKeeper(s.T()) + s.authorityAccount = sdk.AccAddress([]byte("authority")) + s.auctionkeeper = keeper.NewKeeper( + s.encCfg.Codec, + s.key, + s.accountKeeper, + s.bankKeeper, + s.distrKeeper, + s.stakingKeeper, + s.authorityAccount.String(), ) - err := suite.auctionkeeper.SetParams(suite.ctx, types.DefaultParams()) - suite.Require().NoError(err) + err := s.auctionkeeper.SetParams(s.ctx, types.DefaultParams()) + s.Require().NoError(err) - suite.msgServer = keeper.NewMsgServerImpl(suite.auctionkeeper) + s.msgServer = keeper.NewMsgServerImpl(s.auctionkeeper) } diff --git a/x/auction/keeper/msg_server.go b/x/auction/keeper/msg_server.go index 06857c8..65d83eb 100644 --- a/x/auction/keeper/msg_server.go +++ b/x/auction/keeper/msg_server.go @@ -24,59 +24,11 @@ func NewMsgServerImpl(keeper Keeper) *MsgServer { return &MsgServer{Keeper: keeper} } +// AuctionBid is a no-op function that emits an event for the auction bid. The bid is extracted +// in the antehandler. func (m MsgServer) AuctionBid(goCtx context.Context, msg *types.MsgAuctionBid) (*types.MsgAuctionBidResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - // This should never return an error because the address was validated when - // the message was ingressed. - bidder, err := sdk.AccAddressFromBech32(msg.Bidder) - if err != nil { - return nil, err - } - - params, err := m.GetParams(ctx) - if err != nil { - return nil, err - } - - if uint32(len(msg.Transactions)) > params.MaxBundleSize { - return nil, fmt.Errorf("the number of transactions in the bid is greater than the maximum allowed; expected <= %d, got %d", params.MaxBundleSize, len(msg.Transactions)) - } - - escrowAddress := params.EscrowAccountAddress - - var proposerReward sdk.Coins - if params.ProposerFee.IsZero() { - // send the entire bid to the escrow account when no proposer fee is set - if err := m.bankKeeper.SendCoins(ctx, bidder, escrowAddress, sdk.NewCoins(msg.Bid)); err != nil { - return nil, err - } - } else { - rewardsAddress, err := m.rewardsAddressProvider.GetRewardsAddress(ctx) - if err != nil { - // In the case where the rewards address provider returns an error, the - // escrow account will receive the entire bid. - rewardsAddress = escrowAddress - } - - // determine the amount of the bid that goes to the (previous) proposer - bid := sdk.NewDecCoinsFromCoins(msg.Bid) - proposerReward, _ = bid.MulDecTruncate(params.ProposerFee).TruncateDecimal() - - if err := m.bankKeeper.SendCoins(ctx, bidder, rewardsAddress, proposerReward); err != nil { - return nil, err - } - - // Determine the amount of the remaining bid that goes to the escrow account. - // If a decimal remainder exists, it'll stay with the bidding account. - escrowTotal := bid.Sub(sdk.NewDecCoinsFromCoins(proposerReward...)) - escrowReward, _ := escrowTotal.TruncateDecimal() - - if err := m.bankKeeper.SendCoins(ctx, bidder, escrowAddress, escrowReward); err != nil { - return nil, err - } - } - bundledTxHashes := make([]string, len(msg.Transactions)) for i, refTxRaw := range msg.Transactions { hash := sha256.Sum256(refTxRaw) @@ -88,7 +40,6 @@ func (m MsgServer) AuctionBid(goCtx context.Context, msg *types.MsgAuctionBid) ( types.EventTypeAuctionBid, sdk.NewAttribute(types.EventAttrBidder, msg.Bidder), sdk.NewAttribute(types.EventAttrBid, msg.Bid.String()), - sdk.NewAttribute(types.EventAttrProposerReward, proposerReward.String()), sdk.NewAttribute(types.EventAttrBundledTxs, strings.Join(bundledTxHashes, ",")), ), ) diff --git a/x/auction/keeper/msg_server_test.go b/x/auction/keeper/msg_server_test.go index 955a3a8..248d36b 100644 --- a/x/auction/keeper/msg_server_test.go +++ b/x/auction/keeper/msg_server_test.go @@ -6,126 +6,12 @@ import ( "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" testutils "github.com/skip-mev/block-sdk/testutils" "github.com/skip-mev/block-sdk/x/auction/types" ) -func (suite *KeeperTestSuite) TestMsgAuctionBid() { - rng := rand.New(rand.NewSource(time.Now().Unix())) - accounts := testutils.RandomAccounts(rng, 4) - - bidder := accounts[0] - escrow := accounts[1] - - proposerCons := accounts[2] - proposerOperator := accounts[3] - proposer := stakingtypes.Validator{ - OperatorAddress: sdk.ValAddress(proposerOperator.Address).String(), - } - - testCases := []struct { - name string - msg *types.MsgAuctionBid - malleate func() - expectErr bool - }{ - { - name: "invalid bidder address", - msg: &types.MsgAuctionBid{ - Bidder: "stake", - }, - malleate: func() {}, - expectErr: true, - }, - { - name: "too many bundled transactions", - msg: &types.MsgAuctionBid{ - Bidder: bidder.Address.String(), - Transactions: [][]byte{{0xFF}, {0xFF}, {0xFF}}, - }, - malleate: func() { - params := types.DefaultParams() - params.MaxBundleSize = 2 - suite.auctionkeeper.SetParams(suite.ctx, params) - }, - expectErr: true, - }, - { - name: "valid bundle with no proposer fee", - msg: &types.MsgAuctionBid{ - Bidder: bidder.Address.String(), - Bid: sdk.NewInt64Coin("stake", 1024), - Transactions: [][]byte{{0xFF}, {0xFF}}, - }, - malleate: func() { - params := types.DefaultParams() - params.ProposerFee = math.LegacyZeroDec() - params.EscrowAccountAddress = escrow.Address - suite.auctionkeeper.SetParams(suite.ctx, params) - - suite.bankKeeper.EXPECT(). - SendCoins( - suite.ctx, - bidder.Address, - escrow.Address, - sdk.NewCoins(sdk.NewInt64Coin("stake", 1024)), - ). - Return(nil). - AnyTimes() - }, - expectErr: false, - }, - { - name: "valid bundle with proposer fee", - msg: &types.MsgAuctionBid{ - Bidder: bidder.Address.String(), - Bid: sdk.NewInt64Coin("stake", 3416), - Transactions: [][]byte{{0xFF}, {0xFF}}, - }, - malleate: func() { - params := types.DefaultParams() - params.ProposerFee = math.LegacyMustNewDecFromStr("0.30") - params.EscrowAccountAddress = escrow.Address - suite.auctionkeeper.SetParams(suite.ctx, params) - - suite.distrKeeper.EXPECT(). - GetPreviousProposerConsAddr(suite.ctx). - Return(proposerCons.ConsKey.PubKey().Address().Bytes(), nil) - - suite.stakingKeeper.EXPECT(). - GetValidatorByConsAddr(suite.ctx, sdk.ConsAddress(proposerCons.ConsKey.PubKey().Address().Bytes())). - Return(proposer, nil). - AnyTimes() - - suite.bankKeeper.EXPECT(). - SendCoins(suite.ctx, bidder.Address, proposerOperator.Address, sdk.NewCoins(sdk.NewInt64Coin("stake", 1024))). - Return(nil).AnyTimes() - - suite.bankKeeper.EXPECT(). - SendCoins(suite.ctx, bidder.Address, escrow.Address, sdk.NewCoins(sdk.NewInt64Coin("stake", 2392))). - Return(nil).AnyTimes() - }, - expectErr: false, - }, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - tc.malleate() - - _, err := suite.msgServer.AuctionBid(suite.ctx, tc.msg) - if tc.expectErr { - suite.Require().Error(err) - } else { - suite.Require().NoError(err) - } - }) - } -} - -func (suite *KeeperTestSuite) TestMsgUpdateParams() { +func (s *KeeperTestSuite) TestMsgUpdateParams() { rng := rand.New(rand.NewSource(time.Now().Unix())) account := testutils.RandomAccounts(rng, 1)[0] @@ -139,7 +25,7 @@ func (suite *KeeperTestSuite) TestMsgUpdateParams() { { name: "invalid proposer fee", msg: &types.MsgUpdateParams{ - Authority: suite.authorityAccount.String(), + Authority: s.authorityAccount.String(), Params: types.Params{ ProposerFee: math.LegacyMustNewDecFromStr("1.1"), }, @@ -150,7 +36,7 @@ func (suite *KeeperTestSuite) TestMsgUpdateParams() { { name: "invalid auction fees", msg: &types.MsgUpdateParams{ - Authority: suite.authorityAccount.String(), + Authority: s.authorityAccount.String(), Params: types.Params{ ProposerFee: math.LegacyMustNewDecFromStr("0.1"), }, @@ -165,7 +51,7 @@ func (suite *KeeperTestSuite) TestMsgUpdateParams() { Params: types.Params{ ProposerFee: math.LegacyMustNewDecFromStr("0.1"), MaxBundleSize: 2, - EscrowAccountAddress: suite.authorityAccount, + EscrowAccountAddress: s.authorityAccount, MinBidIncrement: sdk.NewInt64Coin("stake", 100), ReserveFee: sdk.NewInt64Coin("stake", 100), }, @@ -176,16 +62,16 @@ func (suite *KeeperTestSuite) TestMsgUpdateParams() { } for _, tc := range testCases { - suite.Run(tc.name, func() { + s.Run(tc.name, func() { if !tc.passBasic { - suite.Require().Error(tc.msg.ValidateBasic()) + s.Require().Error(tc.msg.ValidateBasic()) } - _, err := suite.msgServer.UpdateParams(suite.ctx, tc.msg) + _, err := s.msgServer.UpdateParams(s.ctx, tc.msg) if tc.pass { - suite.Require().NoError(err) + s.Require().NoError(err) } else { - suite.Require().Error(err) + s.Require().Error(err) } }) } diff --git a/x/auction/types/expected_keepers.go b/x/auction/types/expected_keepers.go index 9001f90..c02d6b0 100644 --- a/x/auction/types/expected_keepers.go +++ b/x/auction/types/expected_keepers.go @@ -7,12 +7,16 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) -// BankKeeper defines the expected API contract for the x/auth module. +// BankKeeper defines the expected API contract for the x/account module. +// +//go:generate mockery --name AccountKeeper --output ./mocks --outpkg mocks --case underscore type AccountKeeper interface { GetModuleAddress(moduleName string) sdk.AccAddress } // BankKeeper defines the expected API contract for the x/bank module. +// +//go:generate mockery --name BankKeeper --output ./mocks --outpkg mocks --case underscore type BankKeeper interface { SendCoins(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error GetBalance(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin @@ -20,16 +24,22 @@ type BankKeeper interface { // DistributionKeeper defines the expected API contract for the x/distribution // module. +// +//go:generate mockery --name DistributionKeeper --output ./mocks --outpkg mocks --case underscore type DistributionKeeper interface { GetPreviousProposerConsAddr(ctx context.Context) (sdk.ConsAddress, error) } // StakingKeeper defines the expected API contract for the x/staking module. +// +//go:generate mockery --name StakingKeeper --output ./mocks --outpkg mocks --case underscore type StakingKeeper interface { GetValidatorByConsAddr(context.Context, sdk.ConsAddress) (stakingtypes.Validator, error) } // RewardsAddressProvider is an interface that provides an address where proposer/subset of auction profits are sent. +// +//go:generate mockery --name RewardsAddressProvider --output ./mocks --outpkg mocks --case underscore type RewardsAddressProvider interface { GetRewardsAddress(context sdk.Context) (sdk.AccAddress, error) } diff --git a/x/auction/types/mocks/account_keeper.go b/x/auction/types/mocks/account_keeper.go new file mode 100644 index 0000000..a98ff41 --- /dev/null +++ b/x/auction/types/mocks/account_keeper.go @@ -0,0 +1,43 @@ +// Code generated by mockery v2.30.1. DO NOT EDIT. + +package mocks + +import ( + types "github.com/cosmos/cosmos-sdk/types" + mock "github.com/stretchr/testify/mock" +) + +// AccountKeeper is an autogenerated mock type for the AccountKeeper type +type AccountKeeper struct { + mock.Mock +} + +// GetModuleAddress provides a mock function with given fields: moduleName +func (_m *AccountKeeper) GetModuleAddress(moduleName string) types.AccAddress { + ret := _m.Called(moduleName) + + var r0 types.AccAddress + if rf, ok := ret.Get(0).(func(string) types.AccAddress); ok { + r0 = rf(moduleName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.AccAddress) + } + } + + return r0 +} + +// NewAccountKeeper creates a new instance of AccountKeeper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewAccountKeeper(t interface { + mock.TestingT + Cleanup(func()) +}) *AccountKeeper { + mock := &AccountKeeper{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/x/auction/types/mocks/bank_keeper.go b/x/auction/types/mocks/bank_keeper.go new file mode 100644 index 0000000..e6ebe05 --- /dev/null +++ b/x/auction/types/mocks/bank_keeper.go @@ -0,0 +1,57 @@ +// Code generated by mockery v2.30.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + types "github.com/cosmos/cosmos-sdk/types" + mock "github.com/stretchr/testify/mock" +) + +// BankKeeper is an autogenerated mock type for the BankKeeper type +type BankKeeper struct { + mock.Mock +} + +// GetBalance provides a mock function with given fields: ctx, addr, denom +func (_m *BankKeeper) GetBalance(ctx context.Context, addr types.AccAddress, denom string) types.Coin { + ret := _m.Called(ctx, addr, denom) + + var r0 types.Coin + if rf, ok := ret.Get(0).(func(context.Context, types.AccAddress, string) types.Coin); ok { + r0 = rf(ctx, addr, denom) + } else { + r0 = ret.Get(0).(types.Coin) + } + + return r0 +} + +// SendCoins provides a mock function with given fields: ctx, fromAddr, toAddr, amt +func (_m *BankKeeper) SendCoins(ctx context.Context, fromAddr types.AccAddress, toAddr types.AccAddress, amt types.Coins) error { + ret := _m.Called(ctx, fromAddr, toAddr, amt) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, types.AccAddress, types.AccAddress, types.Coins) error); ok { + r0 = rf(ctx, fromAddr, toAddr, amt) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewBankKeeper creates a new instance of BankKeeper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewBankKeeper(t interface { + mock.TestingT + Cleanup(func()) +}) *BankKeeper { + mock := &BankKeeper{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/x/auction/types/mocks/distribution_keeper.go b/x/auction/types/mocks/distribution_keeper.go new file mode 100644 index 0000000..b64864a --- /dev/null +++ b/x/auction/types/mocks/distribution_keeper.go @@ -0,0 +1,55 @@ +// Code generated by mockery v2.30.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + types "github.com/cosmos/cosmos-sdk/types" + mock "github.com/stretchr/testify/mock" +) + +// DistributionKeeper is an autogenerated mock type for the DistributionKeeper type +type DistributionKeeper struct { + mock.Mock +} + +// GetPreviousProposerConsAddr provides a mock function with given fields: ctx +func (_m *DistributionKeeper) GetPreviousProposerConsAddr(ctx context.Context) (types.ConsAddress, error) { + ret := _m.Called(ctx) + + var r0 types.ConsAddress + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (types.ConsAddress, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) types.ConsAddress); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.ConsAddress) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewDistributionKeeper creates a new instance of DistributionKeeper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDistributionKeeper(t interface { + mock.TestingT + Cleanup(func()) +}) *DistributionKeeper { + mock := &DistributionKeeper{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/x/auction/types/mocks/rewards_address_provider.go b/x/auction/types/mocks/rewards_address_provider.go new file mode 100644 index 0000000..5c740bc --- /dev/null +++ b/x/auction/types/mocks/rewards_address_provider.go @@ -0,0 +1,53 @@ +// Code generated by mockery v2.30.1. DO NOT EDIT. + +package mocks + +import ( + types "github.com/cosmos/cosmos-sdk/types" + mock "github.com/stretchr/testify/mock" +) + +// RewardsAddressProvider is an autogenerated mock type for the RewardsAddressProvider type +type RewardsAddressProvider struct { + mock.Mock +} + +// GetRewardsAddress provides a mock function with given fields: context +func (_m *RewardsAddressProvider) GetRewardsAddress(context types.Context) (types.AccAddress, error) { + ret := _m.Called(context) + + var r0 types.AccAddress + var r1 error + if rf, ok := ret.Get(0).(func(types.Context) (types.AccAddress, error)); ok { + return rf(context) + } + if rf, ok := ret.Get(0).(func(types.Context) types.AccAddress); ok { + r0 = rf(context) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.AccAddress) + } + } + + if rf, ok := ret.Get(1).(func(types.Context) error); ok { + r1 = rf(context) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewRewardsAddressProvider creates a new instance of RewardsAddressProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRewardsAddressProvider(t interface { + mock.TestingT + Cleanup(func()) +}) *RewardsAddressProvider { + mock := &RewardsAddressProvider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/x/auction/types/mocks/staking_keeper.go b/x/auction/types/mocks/staking_keeper.go new file mode 100644 index 0000000..81a2905 --- /dev/null +++ b/x/auction/types/mocks/staking_keeper.go @@ -0,0 +1,55 @@ +// Code generated by mockery v2.30.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + mock "github.com/stretchr/testify/mock" + + types "github.com/cosmos/cosmos-sdk/types" +) + +// StakingKeeper is an autogenerated mock type for the StakingKeeper type +type StakingKeeper struct { + mock.Mock +} + +// GetValidatorByConsAddr provides a mock function with given fields: _a0, _a1 +func (_m *StakingKeeper) GetValidatorByConsAddr(_a0 context.Context, _a1 types.ConsAddress) (stakingtypes.Validator, error) { + ret := _m.Called(_a0, _a1) + + var r0 stakingtypes.Validator + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, types.ConsAddress) (stakingtypes.Validator, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, types.ConsAddress) stakingtypes.Validator); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(stakingtypes.Validator) + } + + if rf, ok := ret.Get(1).(func(context.Context, types.ConsAddress) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewStakingKeeper creates a new instance of StakingKeeper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewStakingKeeper(t interface { + mock.TestingT + Cleanup(func()) +}) *StakingKeeper { + mock := &StakingKeeper{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}