diff --git a/tests/app/ante.go b/tests/app/ante.go index b4ff0a1..b3700ce 100644 --- a/tests/app/ante.go +++ b/tests/app/ante.go @@ -6,18 +6,22 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + + feemarketante "github.com/skip-mev/feemarket/x/feemarket/ante" ) -// HandlerOptions are the options required for constructing an SDK AnteHandler with the fee market injected. -type HandlerOptions struct { - BaseOptions authante.HandlerOptions +// AnteHandlerOptions are the options required for constructing an SDK AnteHandler with the fee market injected. +type AnteHandlerOptions struct { + BaseOptions authante.HandlerOptions + AccountKeeper feemarketante.AccountKeeper + FeeMarketKeeper feemarketante.FeeMarketKeeper } // NewAnteHandler returns an AnteHandler that checks and increments sequence // numbers, checks signatures & account numbers, and deducts fees from the first // signer. -func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { - if options.BaseOptions.AccountKeeper == nil { +func NewAnteHandler(options AnteHandlerOptions) (sdk.AnteHandler, error) { + if options.AccountKeeper == nil { return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "account keeper is required for ante builder") } @@ -29,19 +33,25 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "sign mode handler is required for ante builder") } + if options.FeeMarketKeeper == nil { + return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "feemarket keeper is required for ante builder") + } + anteDecorators := []sdk.AnteDecorator{ authante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first authante.NewExtensionOptionsDecorator(options.BaseOptions.ExtensionOptionChecker), authante.NewValidateBasicDecorator(), authante.NewTxTimeoutHeightDecorator(), - authante.NewValidateMemoDecorator(options.BaseOptions.AccountKeeper), - authante.NewConsumeGasForTxSizeDecorator(options.BaseOptions.AccountKeeper), - authante.NewDeductFeeDecorator(options.BaseOptions.AccountKeeper, options.BaseOptions.BankKeeper, options.BaseOptions.FeegrantKeeper, options.BaseOptions.TxFeeChecker), - authante.NewSetPubKeyDecorator(options.BaseOptions.AccountKeeper), // SetPubKeyDecorator must be called before all signature verification decorators - authante.NewValidateSigCountDecorator(options.BaseOptions.AccountKeeper), - authante.NewSigGasConsumeDecorator(options.BaseOptions.AccountKeeper, options.BaseOptions.SigGasConsumer), - authante.NewSigVerificationDecorator(options.BaseOptions.AccountKeeper, options.BaseOptions.SignModeHandler), - authante.NewIncrementSequenceDecorator(options.BaseOptions.AccountKeeper), + authante.NewValidateMemoDecorator(options.AccountKeeper), + authante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper), + feemarketante.NewFeeMarketCheckDecorator( // fee market check replaces fee deduct decorator + options.FeeMarketKeeper, + ), // fees are deducted in the fee market deduct post handler + authante.NewSetPubKeyDecorator(options.AccountKeeper), // SetPubKeyDecorator must be called before all signature verification decorators + authante.NewValidateSigCountDecorator(options.AccountKeeper), + authante.NewSigGasConsumeDecorator(options.AccountKeeper, options.BaseOptions.SigGasConsumer), + authante.NewSigVerificationDecorator(options.AccountKeeper, options.BaseOptions.SignModeHandler), + authante.NewIncrementSequenceDecorator(options.AccountKeeper), } return sdk.ChainAnteDecorators(anteDecorators...), nil diff --git a/tests/app/app.go b/tests/app/app.go index 2eec646..29af144 100644 --- a/tests/app/app.go +++ b/tests/app/app.go @@ -32,7 +32,7 @@ import ( authzmodule "github.com/cosmos/cosmos-sdk/x/authz/module" "github.com/cosmos/cosmos-sdk/x/bank" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" - consensus "github.com/cosmos/cosmos-sdk/x/consensus" + "github.com/cosmos/cosmos-sdk/x/consensus" consensuskeeper "github.com/cosmos/cosmos-sdk/x/consensus/keeper" "github.com/cosmos/cosmos-sdk/x/crisis" crisiskeeper "github.com/cosmos/cosmos-sdk/x/crisis/keeper" @@ -253,7 +253,7 @@ func New( // Create a global ante handler that will be called on each transaction when // proposals are being built and verified. - handlerOptions := ante.HandlerOptions{ + anteHandlerOptions := ante.HandlerOptions{ AccountKeeper: app.AccountKeeper, BankKeeper: app.BankKeeper, FeegrantKeeper: app.FeeGrantKeeper, @@ -261,12 +261,30 @@ func New( SignModeHandler: app.txConfig.SignModeHandler(), } - anteHandler, err := ante.NewAnteHandler(handlerOptions) + anteOptions := AnteHandlerOptions{ + BaseOptions: anteHandlerOptions, + AccountKeeper: app.AccountKeeper, + FeeMarketKeeper: &app.FeeMarketKeeper, + } + anteHandler, err := NewAnteHandler(anteOptions) + if err != nil { + panic(err) + } + + postHandlerOptions := PostHandlerOptions{ + AccountKeeper: app.AccountKeeper, + BankKeeper: app.BankKeeper, + FeeGrantKeeper: app.FeeGrantKeeper, + FeeMarketKeeper: &app.FeeMarketKeeper, + } + postHandler, err := NewPostHandler(postHandlerOptions) if err != nil { panic(err) } + // set ante and post handlers app.App.SetAnteHandler(anteHandler) + app.App.SetPostHandler(postHandler) // ---------------------------------------------------------------------------- // // ------------------------- End Custom Code ---------------------------------- // diff --git a/tests/app/config.go b/tests/app/config.go index 53b6375..cb34e1a 100644 --- a/tests/app/config.go +++ b/tests/app/config.go @@ -57,15 +57,16 @@ var ( // so that other modules that want to create or claim capabilities afterwards in InitChain // can do so safely. genesisModuleOrder = []string{ - capabilitytypes.ModuleName, authtypes.ModuleName, banktypes.ModuleName, + capabilitytypes.ModuleName, authtypes.ModuleName, banktypes.ModuleName, feemarkettypes.ModuleName, distrtypes.ModuleName, stakingtypes.ModuleName, slashingtypes.ModuleName, govtypes.ModuleName, minttypes.ModuleName, crisistypes.ModuleName, genutiltypes.ModuleName, evidencetypes.ModuleName, authz.ModuleName, feegrant.ModuleName, group.ModuleName, paramstypes.ModuleName, upgradetypes.ModuleName, - vestingtypes.ModuleName, consensustypes.ModuleName, feemarkettypes.ModuleName, + vestingtypes.ModuleName, consensustypes.ModuleName, } // module account permissions moduleAccPerms = []*authmodulev1.ModuleAccountPermission{ + {Account: feemarkettypes.FeeCollectorName, Permissions: []string{authtypes.Burner}}, // allow fee market to burn {Account: authtypes.FeeCollectorName}, {Account: distrtypes.ModuleName}, {Account: minttypes.ModuleName, Permissions: []string{authtypes.Minter}}, @@ -82,6 +83,7 @@ var ( minttypes.ModuleName, stakingtypes.BondedPoolName, stakingtypes.NotBondedPoolName, + feemarkettypes.FeeCollectorName, // We allow the following module accounts to receive funds: // govtypes.ModuleName } @@ -101,6 +103,7 @@ var ( BeginBlockers: []string{ upgradetypes.ModuleName, capabilitytypes.ModuleName, + feemarkettypes.ModuleName, minttypes.ModuleName, distrtypes.ModuleName, slashingtypes.ModuleName, @@ -116,12 +119,12 @@ var ( group.ModuleName, paramstypes.ModuleName, vestingtypes.ModuleName, - feemarkettypes.ModuleName, consensustypes.ModuleName, }, EndBlockers: []string{ crisistypes.ModuleName, govtypes.ModuleName, + feemarkettypes.ModuleName, stakingtypes.ModuleName, capabilitytypes.ModuleName, authtypes.ModuleName, @@ -138,7 +141,6 @@ var ( consensustypes.ModuleName, upgradetypes.ModuleName, vestingtypes.ModuleName, - feemarkettypes.ModuleName, }, OverrideStoreKeys: []*runtimev1alpha1.StoreKeyConfig{ { diff --git a/tests/app/post.go b/tests/app/post.go new file mode 100644 index 0000000..a273c83 --- /dev/null +++ b/tests/app/post.go @@ -0,0 +1,43 @@ +package app + +import ( + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + feemarketpost "github.com/skip-mev/feemarket/x/feemarket/post" +) + +// PostHandlerOptions are the options required for constructing a FeeMarket PostHandler. +type PostHandlerOptions struct { + AccountKeeper feemarketpost.AccountKeeper + BankKeeper feemarketpost.BankKeeper + FeeMarketKeeper feemarketpost.FeeMarketKeeper + FeeGrantKeeper feemarketpost.FeeGrantKeeper +} + +// NewPostHandler returns a PostHandler chain with the fee deduct decorator. +func NewPostHandler(options PostHandlerOptions) (sdk.PostHandler, error) { + if options.AccountKeeper == nil { + return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "account keeper is required for post builder") + } + + if options.BankKeeper == nil { + return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "bank keeper is required for post builder") + } + + if options.FeeMarketKeeper == nil { + return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "feemarket keeper is required for post builder") + } + + postDecorators := []sdk.PostDecorator{ + feemarketpost.NewFeeMarketDeductDecorator( + options.AccountKeeper, + options.BankKeeper, + options.FeeGrantKeeper, + options.FeeMarketKeeper, + ), + } + + return sdk.ChainPostDecorators(postDecorators...), nil +} diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 71e5624..6acf54a 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -17,8 +17,8 @@ import ( var ( // config params - numValidators = 1 - numFullNodes = 0 + numValidators = 3 + numFullNodes = 1 denom = "stake" image = ibc.DockerImage{ @@ -28,7 +28,7 @@ var ( } encodingConfig = MakeEncodingConfig() noHostMount = false - gasAdjustment = 2.0 + gasAdjustment = 10.0 genesisKV = []cosmos.GenesisKV{ { @@ -62,7 +62,7 @@ var ( Bech32Prefix: "cosmos", CoinType: "118", GasAdjustment: gasAdjustment, - GasPrices: fmt.Sprintf("20%s", denom), + GasPrices: fmt.Sprintf("200%s", denom), TrustingPeriod: "48h", NoHostMount: noHostMount, ModifyGenesis: cosmos.ModifyGenesis(genesisKV), diff --git a/tests/integration/setup.go b/tests/integration/setup.go index a92d193..714c620 100644 --- a/tests/integration/setup.go +++ b/tests/integration/setup.go @@ -6,7 +6,9 @@ import ( "context" "encoding/hex" "encoding/json" + "fmt" "io" + "math/rand" "os" "path" "strings" @@ -46,8 +48,8 @@ type KeyringOverride struct { // ChainBuilderFromChainSpec creates an interchaintest chain builder factory given a ChainSpec // and returns the associated chain func ChainBuilderFromChainSpec(t *testing.T, spec *interchaintest.ChainSpec) ibc.Chain { - // require that NumFullNodes == NumValidators == 4 - require.Equal(t, *spec.NumValidators, 1) + // require that NumFullNodes == NumValidators == 3 + require.Equal(t, *spec.NumValidators, 3) cf := interchaintest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*interchaintest.ChainSpec{spec}) @@ -447,3 +449,92 @@ func (s *TestSuite) keyringDirFromNode() string { return localDir } + +// GetAndFundTestUserWithMnemonic restores a user using the given mnemonic +// and funds it with the native chain denom. +// The caller should wait for some blocks to complete before the funds will be accessible. +func (s *TestSuite) GetAndFundTestUserWithMnemonic( + ctx context.Context, + keyNamePrefix, mnemonic string, + amount int64, + chain *cosmos.CosmosChain, +) (ibc.Wallet, error) { + chainCfg := chain.Config() + keyName := fmt.Sprintf("%s-%s-%s", keyNamePrefix, chainCfg.ChainID, RandLowerCaseLetterString(3)) + user, err := chain.BuildWallet(ctx, keyName, mnemonic) + if err != nil { + return nil, fmt.Errorf("failed to get source user wallet: %w", err) + } + + err = chain.SendFunds(ctx, interchaintest.FaucetAccountKeyName, ibc.WalletAmount{ + Address: user.FormattedAddress(), + Amount: math.NewInt(amount), + Denom: chainCfg.Denom, + }) + + _, err = s.ExecTx( + ctx, + chain, + interchaintest.FaucetAccountKeyName, + "bank", + "send", + interchaintest.FaucetAccountKeyName, + user.FormattedAddress(), + fmt.Sprintf("%d%s", amount, chainCfg.Denom), + "--fees", + fmt.Sprintf("%d%s", 200000000000, chainCfg.Denom), + ) + + if err != nil { + return nil, fmt.Errorf("failed to get funds from faucet: %w", err) + } + return user, nil +} + +// GetAndFundTestUsers generates and funds chain users with the native chain denom. +// The caller should wait for some blocks to complete before the funds will be accessible. +func (s *TestSuite) GetAndFundTestUsers( + ctx context.Context, + keyNamePrefix string, + amount int64, + chains ...*cosmos.CosmosChain, +) []ibc.Wallet { + users := make([]ibc.Wallet, len(chains)) + var eg errgroup.Group + for i, chain := range chains { + i := i + chain := chain + eg.Go(func() error { + user, err := s.GetAndFundTestUserWithMnemonic(ctx, keyNamePrefix, "", amount, chain) + if err != nil { + return err + } + users[i] = user + return nil + }) + } + s.Require().NoError(eg.Wait()) + + // TODO(nix 05-17-2022): Map with generics once using go 1.18 + chainHeights := make([]testutil.ChainHeighter, len(chains)) + for i := range chains { + chainHeights[i] = chains[i] + } + return users +} + +func (s *TestSuite) ExecTx(ctx context.Context, chain *cosmos.CosmosChain, keyName string, command ...string) (string, error) { + node := chain.FullNodes[0] + return node.ExecTx(ctx, keyName, command...) +} + +var chars = []byte("abcdefghijklmnopqrstuvwxyz") + +// RandLowerCaseLetterString returns a lowercase letter string of given length +func RandLowerCaseLetterString(length int) string { + b := make([]byte, length) + for i := range b { + b[i] = chars[rand.Intn(len(chars))] + } + return string(b) +} diff --git a/tests/integration/suite.go b/tests/integration/suite.go index ce0276e..b5c973e 100644 --- a/tests/integration/suite.go +++ b/tests/integration/suite.go @@ -75,10 +75,15 @@ func (s *TestSuite) SetupSuite() { ctx := context.Background() s.ic = BuildInterchain(s.T(), ctx, s.chain) + cc, ok := s.chain.(*cosmos.CosmosChain) + if !ok { + panic("unable to assert ibc.Chain as CosmosChain") + } + // get the users - s.user1 = interchaintest.GetAndFundTestUsers(s.T(), ctx, s.T().Name(), initBalance, s.chain)[0] - s.user2 = interchaintest.GetAndFundTestUsers(s.T(), ctx, s.T().Name(), initBalance, s.chain)[0] - s.user3 = interchaintest.GetAndFundTestUsers(s.T(), ctx, s.T().Name(), initBalance, s.chain)[0] + s.user1 = s.GetAndFundTestUsers(ctx, s.T().Name(), initBalance, cc)[0] + s.user2 = s.GetAndFundTestUsers(ctx, s.T().Name(), initBalance, cc)[0] + s.user3 = s.GetAndFundTestUsers(ctx, s.T().Name(), initBalance, cc)[0] // create the broadcaster s.T().Log("creating broadcaster") diff --git a/testutils/testutils.go b/testutils/testutils.go index 5bc9d8c..2ed04ae 100644 --- a/testutils/testutils.go +++ b/testutils/testutils.go @@ -3,6 +3,8 @@ package testutils import ( "math/rand" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec/types" @@ -28,21 +30,22 @@ type EncodingConfig struct { } func CreateTestEncodingConfig() EncodingConfig { - cdc := codec.NewLegacyAmino() + amino := codec.NewLegacyAmino() interfaceRegistry := types.NewInterfaceRegistry() banktypes.RegisterInterfaces(interfaceRegistry) + authtypes.RegisterInterfaces(interfaceRegistry) cryptocodec.RegisterInterfaces(interfaceRegistry) feemarkettypes.RegisterInterfaces(interfaceRegistry) stakingtypes.RegisterInterfaces(interfaceRegistry) - codec := codec.NewProtoCodec(interfaceRegistry) + cdc := codec.NewProtoCodec(interfaceRegistry) return EncodingConfig{ InterfaceRegistry: interfaceRegistry, - Codec: codec, - TxConfig: tx.NewTxConfig(codec, tx.DefaultSignModes), - Amino: cdc, + Codec: cdc, + TxConfig: tx.NewTxConfig(cdc, tx.DefaultSignModes), + Amino: amino, } } diff --git a/x/feemarket/ante/expected_keepers.go b/x/feemarket/ante/expected_keepers.go index bd31753..c3c2592 100644 --- a/x/feemarket/ante/expected_keepers.go +++ b/x/feemarket/ante/expected_keepers.go @@ -2,26 +2,44 @@ package ante import ( sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + feemarkettypes "github.com/skip-mev/feemarket/x/feemarket/types" ) // AccountKeeper defines the contract needed for AccountKeeper related APIs. // Interface provides support to use non-sdk AccountKeeper for AnteHandler's decorators. +// +//go:generate mockery --name AccountKeeper --filename mock_account_keeper.go type AccountKeeper interface { - GetParams(ctx sdk.Context) (params types.Params) - GetAccount(ctx sdk.Context, addr sdk.AccAddress) types.AccountI - SetAccount(ctx sdk.Context, acc types.AccountI) + GetParams(ctx sdk.Context) (params authtypes.Params) + GetAccount(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI + SetAccount(ctx sdk.Context, acc authtypes.AccountI) GetModuleAddress(moduleName string) sdk.AccAddress + GetModuleAccount(ctx sdk.Context, name string) authtypes.ModuleAccountI + NewAccountWithAddress(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI } -// FeegrantKeeper defines the expected feegrant keeper. -type FeegrantKeeper interface { +// FeeGrantKeeper defines the expected feegrant keeper. +// +//go:generate mockery --name FeeGrantKeeper --filename mock_feegrant_keeper.go +type FeeGrantKeeper interface { UseGrantedFees(ctx sdk.Context, granter, grantee sdk.AccAddress, fee sdk.Coins, msgs []sdk.Msg) error } -// BankKeeper defines the contract needed for supply related APIs (noalias) +// BankKeeper defines the contract needed for supply related APIs. +// +//go:generate mockery --name BankKeeper --filename mock_bank_keeper.go type BankKeeper interface { IsSendEnabledCoins(ctx sdk.Context, coins ...sdk.Coin) error SendCoins(ctx sdk.Context, from, to sdk.AccAddress, amt sdk.Coins) error SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error } + +// FeeMarketKeeper defines the expected feemarket keeper. +// +//go:generate mockery --name FeeMarketKeeper --filename mock_feemarket_keeper.go +type FeeMarketKeeper interface { + GetState(ctx sdk.Context) (feemarkettypes.State, error) + GetMinGasPrices(ctx sdk.Context) (sdk.Coins, error) +} diff --git a/x/feemarket/ante/fee.go b/x/feemarket/ante/fee.go new file mode 100644 index 0000000..709a070 --- /dev/null +++ b/x/feemarket/ante/fee.go @@ -0,0 +1,113 @@ +package ante + +import ( + "math" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// FeeMarketCheckDecorator checks sufficient fees from the fee payer based off of the current +// state of the feemarket. +// If the fee payer does not have the funds to pay for the fees, return an InsufficientFunds error. +// Call next AnteHandler if fees successfully checked. +// CONTRACT: Tx must implement FeeTx interface +type FeeMarketCheckDecorator struct { + feemarketKeeper FeeMarketKeeper +} + +func NewFeeMarketCheckDecorator(fmk FeeMarketKeeper) FeeMarketCheckDecorator { + return FeeMarketCheckDecorator{ + feemarketKeeper: fmk, + } +} + +// AnteHandle checks if the tx provides sufficient fee to cover the required fee from the fee market. +func (dfd FeeMarketCheckDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + // GenTx consume no fee + if ctx.BlockHeight() == 0 { + return next(ctx, tx, simulate) + } + + feeTx, ok := tx.(sdk.FeeTx) + if !ok { + return ctx, errorsmod.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx") + } + + if !simulate && ctx.BlockHeight() > 0 && feeTx.GetGas() == 0 { + return ctx, errorsmod.Wrap(sdkerrors.ErrInvalidGasLimit, "must provide positive gas") + } + + minGasPrices, err := dfd.feemarketKeeper.GetMinGasPrices(ctx) + if err != nil { + return ctx, errorsmod.Wrapf(err, "unable to get fee market state") + } + + fee := feeTx.GetFee() + gas := feeTx.GetGas() // use provided gas limit + + if !simulate { + fee, _, err = CheckTxFees(minGasPrices, feeTx, gas) + if err != nil { + return ctx, err + } + } + + minGasPricesDecCoins := sdk.NewDecCoinsFromCoins(minGasPrices...) + newCtx := ctx.WithPriority(getTxPriority(fee, int64(gas))).WithMinGasPrices(minGasPricesDecCoins) + return next(newCtx, tx, simulate) +} + +// CheckTxFees implements the logic for the fee market to check if a Tx has provided sufficient +// fees given the current state of the fee market. Returns an error if insufficient fees. +func CheckTxFees(minFees sdk.Coins, feeTx sdk.FeeTx, gas uint64) (feeCoins sdk.Coins, tip sdk.Coins, err error) { + feesDec := sdk.NewDecCoinsFromCoins(minFees...) + + feeCoins = feeTx.GetFee() + + // Ensure that the provided fees meet the minimum + minGasPrices := feesDec + if !minGasPrices.IsZero() { + requiredFees := make(sdk.Coins, len(minGasPrices)) + + // Determine the required fees by multiplying each required minimum gas + // price by the gas, where fee = ceil(minGasPrice * gas). + glDec := sdkmath.LegacyNewDec(int64(gas)) + for i, gp := range minGasPrices { + fee := gp.Amount.Mul(glDec) + requiredFees[i] = sdk.NewCoin(gp.Denom, fee.Ceil().RoundInt()) + } + + if !feeCoins.IsAnyGTE(requiredFees) { + return nil, nil, errorsmod.Wrapf(sdkerrors.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins, requiredFees) + } + + tip = feeCoins.Sub(minFees...) // tip is the difference between feeCoins and the min fees + feeCoins = requiredFees // set fee coins to be ONLY the required amount + } + + return feeCoins, tip, nil +} + +// getTxPriority returns a naive tx priority based on the amount of the smallest denomination of the gas price +// provided in a transaction. +// NOTE: This implementation should be used with a great consideration as it opens potential attack vectors +// where txs with multiple coins could not be prioritized as expected. +func getTxPriority(fee sdk.Coins, gas int64) int64 { + var priority int64 + for _, c := range fee { + p := int64(math.MaxInt64) + gasPrice := c.Amount.QuoRaw(gas) + if gasPrice.IsInt64() { + p = gasPrice.Int64() + } + if priority == 0 || p < priority { + priority = p + } + } + + return priority +} diff --git a/x/feemarket/ante/fee_test.go b/x/feemarket/ante/fee_test.go new file mode 100644 index 0000000..c8a4b4a --- /dev/null +++ b/x/feemarket/ante/fee_test.go @@ -0,0 +1,67 @@ +package ante_test + +import ( + "fmt" + "testing" + + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + _ "github.com/cosmos/cosmos-sdk/x/auth" + + antesuite "github.com/skip-mev/feemarket/x/feemarket/ante/suite" + "github.com/skip-mev/feemarket/x/feemarket/types" +) + +func TestAnteHandle(t *testing.T) { + // Same data for every test case + gasLimit := antesuite.NewTestGasLimit() + validFeeAmount := types.DefaultMinBaseFee.MulRaw(int64(gasLimit)) + validFee := sdk.NewCoins(sdk.NewCoin("stake", validFeeAmount)) + + testCases := []antesuite.TestCase{ + { + Name: "0 gas given should fail", + Malleate: func(suite *antesuite.TestSuite) antesuite.TestCaseArgs { + accs := suite.CreateTestAccounts(1) + + return antesuite.TestCaseArgs{ + Msgs: []sdk.Msg{testdata.NewTestMsg(accs[0].Account.GetAddress())}, + GasLimit: 0, + FeeAmount: validFee, + } + }, + RunAnte: true, + RunPost: false, + Simulate: false, + ExpPass: false, + ExpErr: sdkerrors.ErrInvalidGasLimit, + }, + { + Name: "signer has enough funds, should pass", + Malleate: func(suite *antesuite.TestSuite) antesuite.TestCaseArgs { + accs := suite.CreateTestAccounts(1) + return antesuite.TestCaseArgs{ + Msgs: []sdk.Msg{testdata.NewTestMsg(accs[0].Account.GetAddress())}, + GasLimit: gasLimit, + FeeAmount: validFee, + } + }, + RunAnte: true, + RunPost: false, + Simulate: false, + ExpPass: true, + ExpErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("Case %s", tc.Name), func(t *testing.T) { + s := antesuite.SetupTestSuite(t) + s.TxBuilder = s.ClientCtx.TxConfig.NewTxBuilder() + args := tc.Malleate(s) + + s.RunTestCase(t, tc, args) + }) + } +} diff --git a/x/feemarket/ante/mocks/mock_account_keeper.go b/x/feemarket/ante/mocks/mock_account_keeper.go new file mode 100644 index 0000000..fb833d3 --- /dev/null +++ b/x/feemarket/ante/mocks/mock_account_keeper.go @@ -0,0 +1,113 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + mock "github.com/stretchr/testify/mock" + + types "github.com/cosmos/cosmos-sdk/types" +) + +// AccountKeeper is an autogenerated mock type for the AccountKeeper type +type AccountKeeper struct { + mock.Mock +} + +// GetAccount provides a mock function with given fields: ctx, addr +func (_m *AccountKeeper) GetAccount(ctx types.Context, addr types.AccAddress) authtypes.AccountI { + ret := _m.Called(ctx, addr) + + var r0 authtypes.AccountI + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress) authtypes.AccountI); ok { + r0 = rf(ctx, addr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(authtypes.AccountI) + } + } + + return r0 +} + +// GetModuleAccount provides a mock function with given fields: ctx, name +func (_m *AccountKeeper) GetModuleAccount(ctx types.Context, name string) authtypes.ModuleAccountI { + ret := _m.Called(ctx, name) + + var r0 authtypes.ModuleAccountI + if rf, ok := ret.Get(0).(func(types.Context, string) authtypes.ModuleAccountI); ok { + r0 = rf(ctx, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(authtypes.ModuleAccountI) + } + } + + return r0 +} + +// 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 +} + +// GetParams provides a mock function with given fields: ctx +func (_m *AccountKeeper) GetParams(ctx types.Context) authtypes.Params { + ret := _m.Called(ctx) + + var r0 authtypes.Params + if rf, ok := ret.Get(0).(func(types.Context) authtypes.Params); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(authtypes.Params) + } + + return r0 +} + +// NewAccountWithAddress provides a mock function with given fields: ctx, addr +func (_m *AccountKeeper) NewAccountWithAddress(ctx types.Context, addr types.AccAddress) authtypes.AccountI { + ret := _m.Called(ctx, addr) + + var r0 authtypes.AccountI + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress) authtypes.AccountI); ok { + r0 = rf(ctx, addr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(authtypes.AccountI) + } + } + + return r0 +} + +// SetAccount provides a mock function with given fields: ctx, acc +func (_m *AccountKeeper) SetAccount(ctx types.Context, acc authtypes.AccountI) { + _m.Called(ctx, acc) +} + +// 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/feemarket/ante/mocks/mock_bank_keeper.go b/x/feemarket/ante/mocks/mock_bank_keeper.go new file mode 100644 index 0000000..1cbcf2a --- /dev/null +++ b/x/feemarket/ante/mocks/mock_bank_keeper.go @@ -0,0 +1,77 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + 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 +} + +// IsSendEnabledCoins provides a mock function with given fields: ctx, coins +func (_m *BankKeeper) IsSendEnabledCoins(ctx types.Context, coins ...types.Coin) error { + _va := make([]interface{}, len(coins)) + for _i := range coins { + _va[_i] = coins[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, ...types.Coin) error); ok { + r0 = rf(ctx, coins...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendCoins provides a mock function with given fields: ctx, from, to, amt +func (_m *BankKeeper) SendCoins(ctx types.Context, from types.AccAddress, to types.AccAddress, amt types.Coins) error { + ret := _m.Called(ctx, from, to, amt) + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, types.AccAddress, types.Coins) error); ok { + r0 = rf(ctx, from, to, amt) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendCoinsFromAccountToModule provides a mock function with given fields: ctx, senderAddr, recipientModule, amt +func (_m *BankKeeper) SendCoinsFromAccountToModule(ctx types.Context, senderAddr types.AccAddress, recipientModule string, amt types.Coins) error { + ret := _m.Called(ctx, senderAddr, recipientModule, amt) + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, string, types.Coins) error); ok { + r0 = rf(ctx, senderAddr, recipientModule, 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/feemarket/ante/mocks/mock_feegrant_keeper.go b/x/feemarket/ante/mocks/mock_feegrant_keeper.go new file mode 100644 index 0000000..2a65489 --- /dev/null +++ b/x/feemarket/ante/mocks/mock_feegrant_keeper.go @@ -0,0 +1,42 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + types "github.com/cosmos/cosmos-sdk/types" + mock "github.com/stretchr/testify/mock" +) + +// FeeGrantKeeper is an autogenerated mock type for the FeeGrantKeeper type +type FeeGrantKeeper struct { + mock.Mock +} + +// UseGrantedFees provides a mock function with given fields: ctx, granter, grantee, fee, msgs +func (_m *FeeGrantKeeper) UseGrantedFees(ctx types.Context, granter types.AccAddress, grantee types.AccAddress, fee types.Coins, msgs []types.Msg) error { + ret := _m.Called(ctx, granter, grantee, fee, msgs) + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, types.AccAddress, types.Coins, []types.Msg) error); ok { + r0 = rf(ctx, granter, grantee, fee, msgs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewFeeGrantKeeper creates a new instance of FeeGrantKeeper. 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 NewFeeGrantKeeper(t interface { + mock.TestingT + Cleanup(func()) +}, +) *FeeGrantKeeper { + mock := &FeeGrantKeeper{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/x/feemarket/ante/mocks/mock_feemarket_keeper.go b/x/feemarket/ante/mocks/mock_feemarket_keeper.go new file mode 100644 index 0000000..aa59895 --- /dev/null +++ b/x/feemarket/ante/mocks/mock_feemarket_keeper.go @@ -0,0 +1,81 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + + feemarkettypes "github.com/skip-mev/feemarket/x/feemarket/types" + + types "github.com/cosmos/cosmos-sdk/types" +) + +// FeeMarketKeeper is an autogenerated mock type for the FeeMarketKeeper type +type FeeMarketKeeper struct { + mock.Mock +} + +// GetMinGasPrices provides a mock function with given fields: ctx +func (_m *FeeMarketKeeper) GetMinGasPrices(ctx types.Context) (types.Coins, error) { + ret := _m.Called(ctx) + + var r0 types.Coins + var r1 error + if rf, ok := ret.Get(0).(func(types.Context) (types.Coins, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(types.Context) types.Coins); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.Coins) + } + } + + if rf, ok := ret.Get(1).(func(types.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetState provides a mock function with given fields: ctx +func (_m *FeeMarketKeeper) GetState(ctx types.Context) (feemarkettypes.State, error) { + ret := _m.Called(ctx) + + var r0 feemarkettypes.State + var r1 error + if rf, ok := ret.Get(0).(func(types.Context) (feemarkettypes.State, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(types.Context) feemarkettypes.State); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(feemarkettypes.State) + } + + if rf, ok := ret.Get(1).(func(types.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewFeeMarketKeeper creates a new instance of FeeMarketKeeper. 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 NewFeeMarketKeeper(t interface { + mock.TestingT + Cleanup(func()) +}, +) *FeeMarketKeeper { + mock := &FeeMarketKeeper{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/x/feemarket/ante/suite/suite.go b/x/feemarket/ante/suite/suite.go new file mode 100644 index 0000000..4151ee3 --- /dev/null +++ b/x/feemarket/ante/suite/suite.go @@ -0,0 +1,275 @@ +package suite + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + "github.com/cosmos/cosmos-sdk/testutil" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/skip-mev/feemarket/testutils" + feemarketante "github.com/skip-mev/feemarket/x/feemarket/ante" + "github.com/skip-mev/feemarket/x/feemarket/ante/mocks" + "github.com/skip-mev/feemarket/x/feemarket/keeper" + feemarketpost "github.com/skip-mev/feemarket/x/feemarket/post" + "github.com/skip-mev/feemarket/x/feemarket/types" +) + +type TestSuite struct { + suite.Suite + + Ctx sdk.Context + AnteHandler sdk.AnteHandler + PostHandler sdk.PostHandler + ClientCtx client.Context + TxBuilder client.TxBuilder + + AccountKeeper authkeeper.AccountKeeper + FeemarketKeeper *keeper.Keeper + BankKeeper *mocks.BankKeeper + FeeGrantKeeper *mocks.FeeGrantKeeper + EncCfg testutils.EncodingConfig + Key *storetypes.KVStoreKey + AuthorityAccount sdk.AccAddress +} + +// TestAccount represents an account used in the tests in x/auth/ante. +type TestAccount struct { + Account authtypes.AccountI + Priv cryptotypes.PrivKey +} + +func (s *TestSuite) CreateTestAccounts(numAccs int) []TestAccount { + var accounts []TestAccount + + for i := 0; i < numAccs; i++ { + priv, _, addr := testdata.KeyTestPubAddr() + acc := s.AccountKeeper.NewAccountWithAddress(s.Ctx, addr) + err := acc.SetAccountNumber(uint64(i + 1000)) + if err != nil { + panic(err) + } + s.AccountKeeper.SetAccount(s.Ctx, acc) + accounts = append(accounts, TestAccount{acc, priv}) + } + + return accounts +} + +// SetupTest setups a new test, with new app, context, and anteHandler. +func SetupTestSuite(t *testing.T) *TestSuite { + s := &TestSuite{} + + s.EncCfg = testutils.CreateTestEncodingConfig() + s.Key = storetypes.NewKVStoreKey(types.StoreKey) + tkey := storetypes.NewTransientStoreKey("transient_test_feemarket") + testCtx := testutil.DefaultContextWithDB(t, s.Key, tkey) + s.Ctx = testCtx.Ctx.WithIsCheckTx(false).WithBlockHeight(1) + cms, db := testCtx.CMS, testCtx.DB + + authKey := storetypes.NewKVStoreKey(authtypes.StoreKey) + tkey = storetypes.NewTransientStoreKey("transient_test_auth") + cms.MountStoreWithDB(authKey, storetypes.StoreTypeIAVL, db) + cms.MountStoreWithDB(tkey, storetypes.StoreTypeTransient, db) + err := cms.LoadLatestVersion() + require.NoError(t, err) + + maccPerms := map[string][]string{ + types.ModuleName: nil, + types.FeeCollectorName: {"burner"}, + } + + s.AuthorityAccount = authtypes.NewModuleAddress("gov") + s.AccountKeeper = authkeeper.NewAccountKeeper( + s.EncCfg.Codec, authKey, authtypes.ProtoBaseAccount, maccPerms, sdk.Bech32MainPrefix, s.AuthorityAccount.String(), + ) + + s.FeemarketKeeper = keeper.NewKeeper( + s.EncCfg.Codec, + s.Key, + s.AccountKeeper, + s.AuthorityAccount.String(), + ) + + err = s.FeemarketKeeper.SetParams(s.Ctx, types.DefaultParams()) + require.NoError(t, err) + + err = s.FeemarketKeeper.SetState(s.Ctx, types.DefaultState()) + require.NoError(t, err) + + s.BankKeeper = mocks.NewBankKeeper(t) + s.FeeGrantKeeper = mocks.NewFeeGrantKeeper(t) + + s.ClientCtx = client.Context{}.WithTxConfig(s.EncCfg.TxConfig) + s.TxBuilder = s.ClientCtx.TxConfig.NewTxBuilder() + + // create basic antehandler with the feemarket decorator + anteDecorators := []sdk.AnteDecorator{ + authante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first + feemarketante.NewFeeMarketCheckDecorator( // fee market replaces fee deduct decorator + s.FeemarketKeeper, + ), + authante.NewSigGasConsumeDecorator(s.AccountKeeper, authante.DefaultSigVerificationGasConsumer), + } + + s.AnteHandler = sdk.ChainAnteDecorators(anteDecorators...) + + // create basic postHandler with the feemarket decorator + postDecorators := []sdk.PostDecorator{ + feemarketpost.NewFeeMarketDeductDecorator( + s.AccountKeeper, + s.BankKeeper, + s.FeeGrantKeeper, + s.FeemarketKeeper, + ), + } + + s.PostHandler = sdk.ChainPostDecorators(postDecorators...) + return s +} + +// TestCase represents a test case used in test tables. +type TestCase struct { + Name string + Malleate func(*TestSuite) TestCaseArgs + RunAnte bool + RunPost bool + Simulate bool + ExpPass bool + ExpErr error +} + +type TestCaseArgs struct { + ChainID string + AccNums []uint64 + AccSeqs []uint64 + FeeAmount sdk.Coins + GasLimit uint64 + Msgs []sdk.Msg + Privs []cryptotypes.PrivKey +} + +// DeliverMsgs constructs a tx and runs it through the ante handler. This is used to set the context for a test case, for +// example to test for replay protection. +func (s *TestSuite) DeliverMsgs(t *testing.T, privs []cryptotypes.PrivKey, msgs []sdk.Msg, feeAmount sdk.Coins, gasLimit uint64, accNums, accSeqs []uint64, chainID string, simulate bool) (sdk.Context, error) { + require.NoError(t, s.TxBuilder.SetMsgs(msgs...)) + s.TxBuilder.SetFeeAmount(feeAmount) + s.TxBuilder.SetGasLimit(gasLimit) + + tx, txErr := s.CreateTestTx(privs, accNums, accSeqs, chainID) + require.NoError(t, txErr) + return s.AnteHandler(s.Ctx, tx, simulate) +} + +func (s *TestSuite) RunTestCase(t *testing.T, tc TestCase, args TestCaseArgs) { + require.NoError(t, s.TxBuilder.SetMsgs(args.Msgs...)) + s.TxBuilder.SetFeeAmount(args.FeeAmount) + s.TxBuilder.SetGasLimit(args.GasLimit) + + // Theoretically speaking, ante handler unit tests should only test + // ante handlers, but here we sometimes also test the tx creation + // process. + tx, txErr := s.CreateTestTx(args.Privs, args.AccNums, args.AccSeqs, args.ChainID) + + var ( + newCtx sdk.Context + handleErr error + ) + + if tc.RunAnte { + newCtx, handleErr = s.AnteHandler(s.Ctx, tx, tc.Simulate) + } + + if tc.RunPost { + newCtx, handleErr = s.PostHandler(s.Ctx, tx, tc.Simulate, true) + } + + if tc.ExpPass { + require.NoError(t, txErr) + require.NoError(t, handleErr) + require.NotNil(t, newCtx) + + s.Ctx = newCtx + } else { + switch { + case txErr != nil: + require.Error(t, txErr) + require.ErrorIs(t, txErr, tc.ExpErr) + + case handleErr != nil: + require.Error(t, handleErr) + require.ErrorIs(t, handleErr, tc.ExpErr) + + default: + t.Fatal("expected one of txErr, handleErr to be an error") + } + } +} + +// CreateTestTx is a helper function to create a tx given multiple inputs. +func (s *TestSuite) CreateTestTx(privs []cryptotypes.PrivKey, accNums []uint64, accSeqs []uint64, chainID string) (authsigning.Tx, error) { + // First round: we gather all the signer infos. We use the "set empty + // signature" hack to do that. + var sigsV2 []signing.SignatureV2 + for i, priv := range privs { + sigV2 := signing.SignatureV2{ + PubKey: priv.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: s.ClientCtx.TxConfig.SignModeHandler().DefaultMode(), + Signature: nil, + }, + Sequence: accSeqs[i], + } + + sigsV2 = append(sigsV2, sigV2) + } + err := s.TxBuilder.SetSignatures(sigsV2...) + if err != nil { + return nil, err + } + + // Second round: all signer infos are set, so each signer can sign. + sigsV2 = []signing.SignatureV2{} + for i, priv := range privs { + signerData := authsigning.SignerData{ + ChainID: chainID, + AccountNumber: accNums[i], + Sequence: accSeqs[i], + } + sigV2, err := tx.SignWithPrivKey( + s.ClientCtx.TxConfig.SignModeHandler().DefaultMode(), signerData, + s.TxBuilder, priv, s.ClientCtx.TxConfig, accSeqs[i]) + if err != nil { + return nil, err + } + + sigsV2 = append(sigsV2, sigV2) + } + err = s.TxBuilder.SetSignatures(sigsV2...) + if err != nil { + return nil, err + } + + return s.TxBuilder.GetTx(), nil +} + +// NewTestFeeAmount is a test fee amount. +func NewTestFeeAmount() sdk.Coins { + return sdk.NewCoins(sdk.NewInt64Coin("stake", 150)) +} + +// NewTestGasLimit is a test fee gas limit. +func NewTestGasLimit() uint64 { + return 200000 +} diff --git a/x/feemarket/keeper/keeper.go b/x/feemarket/keeper/keeper.go index fb59bcc..90a8cb7 100644 --- a/x/feemarket/keeper/keeper.go +++ b/x/feemarket/keeper/keeper.go @@ -1,6 +1,8 @@ package keeper import ( + "fmt" + "github.com/cometbft/cometbft/libs/log" "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" @@ -12,6 +14,7 @@ import ( type Keeper struct { cdc codec.BinaryCodec storeKey storetypes.StoreKey + ak types.AccountKeeper // The address that is capable of executing a MsgParams message. // Typically, this will be the governance module's address. @@ -22,11 +25,22 @@ type Keeper struct { func NewKeeper( cdc codec.BinaryCodec, storeKey storetypes.StoreKey, + authKeeper types.AccountKeeper, authority string, ) *Keeper { + // ensure governance module account is set + if addr := authKeeper.GetModuleAddress(types.FeeCollectorName); addr == nil { + panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) + } + + if _, err := sdk.AccAddressFromBech32(authority); err != nil { + panic(fmt.Sprintf("invalid authority address: %s", authority)) + } + k := &Keeper{ cdc, storeKey, + authKeeper, authority, } diff --git a/x/feemarket/keeper/keeper_test.go b/x/feemarket/keeper/keeper_test.go index 461bbba..75694ef 100644 --- a/x/feemarket/keeper/keeper_test.go +++ b/x/feemarket/keeper/keeper_test.go @@ -12,11 +12,13 @@ import ( "github.com/skip-mev/feemarket/testutils" "github.com/skip-mev/feemarket/x/feemarket/keeper" "github.com/skip-mev/feemarket/x/feemarket/types" + "github.com/skip-mev/feemarket/x/feemarket/types/mocks" ) type KeeperTestSuite struct { suite.Suite + accountKeeper *mocks.AccountKeeper feemarketKeeper *keeper.Keeper encCfg testutils.EncodingConfig ctx sdk.Context @@ -41,9 +43,13 @@ func (s *KeeperTestSuite) SetupTest() { s.ctx = testCtx.Ctx s.authorityAccount = []byte("authority") + s.accountKeeper = mocks.NewAccountKeeper(s.T()) + s.accountKeeper.On("GetModuleAddress", "feemarket-fee-collector").Return(sdk.AccAddress("feemarket-fee-collector")) + s.feemarketKeeper = keeper.NewKeeper( s.encCfg.Codec, s.key, + s.accountKeeper, s.authorityAccount.String(), ) diff --git a/x/feemarket/module.go b/x/feemarket/module.go index 9080ae1..d531d9a 100644 --- a/x/feemarket/module.go +++ b/x/feemarket/module.go @@ -158,9 +158,10 @@ func init() { type Inputs struct { depinject.In - Config *modulev1.Module - Cdc codec.Codec - Key *store.KVStoreKey + Config *modulev1.Module + Cdc codec.Codec + Key *store.KVStoreKey + AccountKeeper types.AccountKeeper } type Outputs struct { @@ -187,6 +188,7 @@ func ProvideModule(in Inputs) Outputs { Keeper := keeper.NewKeeper( in.Cdc, in.Key, + in.AccountKeeper, authority.String(), ) diff --git a/x/feemarket/post/expected_keeper.go b/x/feemarket/post/expected_keeper.go new file mode 100644 index 0000000..246e07d --- /dev/null +++ b/x/feemarket/post/expected_keeper.go @@ -0,0 +1,46 @@ +package post + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + feemarkettypes "github.com/skip-mev/feemarket/x/feemarket/types" +) + +// AccountKeeper defines the contract needed for AccountKeeper related APIs. +// Interface provides support to use non-sdk AccountKeeper for AnteHandler's decorators. +// +//go:generate mockery --name AccountKeeper --filename mock_account_keeper.go +type AccountKeeper interface { + GetParams(ctx sdk.Context) (params authtypes.Params) + GetAccount(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI + SetAccount(ctx sdk.Context, acc authtypes.AccountI) + GetModuleAddress(moduleName string) sdk.AccAddress + GetModuleAccount(ctx sdk.Context, name string) authtypes.ModuleAccountI + NewAccountWithAddress(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI +} + +// FeeGrantKeeper defines the expected feegrant keeper. +// +//go:generate mockery --name FeeGrantKeeper --filename mock_feegrant_keeper.go +type FeeGrantKeeper interface { + UseGrantedFees(ctx sdk.Context, granter, grantee sdk.AccAddress, fee sdk.Coins, msgs []sdk.Msg) error +} + +// BankKeeper defines the contract needed for supply related APIs. +// +//go:generate mockery --name BankKeeper --filename mock_bank_keeper.go +type BankKeeper interface { + IsSendEnabledCoins(ctx sdk.Context, coins ...sdk.Coin) error + SendCoins(ctx sdk.Context, from, to sdk.AccAddress, amt sdk.Coins) error + SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error +} + +// FeeMarketKeeper defines the expected feemarket keeper. +// +//go:generate mockery --name FeeMarketKeeper --filename mock_feemarket_keeper.go +type FeeMarketKeeper interface { + GetState(ctx sdk.Context) (feemarkettypes.State, error) + SetState(ctx sdk.Context, state feemarkettypes.State) error + GetMinGasPrices(ctx sdk.Context) (sdk.Coins, error) +} diff --git a/x/feemarket/post/fee.go b/x/feemarket/post/fee.go new file mode 100644 index 0000000..425c607 --- /dev/null +++ b/x/feemarket/post/fee.go @@ -0,0 +1,187 @@ +package post + +import ( + "fmt" + + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + "github.com/skip-mev/feemarket/x/feemarket/ante" + feemarkettypes "github.com/skip-mev/feemarket/x/feemarket/types" +) + +// FeeMarketDeductDecorator deducts fees from the fee payer based off of the current state of the feemarket. +// The fee payer is the fee granter (if specified) or first signer of the tx. +// If the fee payer does not have the funds to pay for the fees, return an InsufficientFunds error. +// If there is an excess between the given fee and the on-chain min base fee is given as a tip. +// Call next PostHandler if fees successfully deducted. +// CONTRACT: Tx must implement FeeTx interface +type FeeMarketDeductDecorator struct { + accountKeeper AccountKeeper + bankKeeper BankKeeper + feegrantKeeper FeeGrantKeeper + feemarketKeeper FeeMarketKeeper +} + +func NewFeeMarketDeductDecorator(ak AccountKeeper, bk BankKeeper, fk FeeGrantKeeper, fmk FeeMarketKeeper) FeeMarketDeductDecorator { + return FeeMarketDeductDecorator{ + accountKeeper: ak, + bankKeeper: bk, + feegrantKeeper: fk, + feemarketKeeper: fmk, + } +} + +// PostHandle deducts the fee from the fee payer based on the min base fee and the gas consumed in the gasmeter. +// If there is a difference between the provided fee and the min-base fee, the difference is paid as a tip. +// Fees are sent to the x/feemarket fee-collector address. +func (dfd FeeMarketDeductDecorator) PostHandle(ctx sdk.Context, tx sdk.Tx, simulate, success bool, next sdk.PostHandler) (sdk.Context, error) { + // GenTx consume no fee + if ctx.BlockHeight() == 0 { + return next(ctx, tx, simulate, success) + } + + feeTx, ok := tx.(sdk.FeeTx) + if !ok { + return ctx, errorsmod.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx") + } + + if !simulate && ctx.BlockHeight() > 0 && feeTx.GetGas() == 0 { + return ctx, errorsmod.Wrap(sdkerrors.ErrInvalidGasLimit, "must provide positive gas") + } + + var tip sdk.Coins + + minGasPrices, err := dfd.feemarketKeeper.GetMinGasPrices(ctx) + if err != nil { + return ctx, errorsmod.Wrapf(err, "unable to get fee market state") + } + + fee := feeTx.GetFee() + gas := ctx.GasMeter().GasConsumed() // use context gas consumed + + if !simulate { + fee, tip, err = ante.CheckTxFees(minGasPrices, feeTx, gas) + if err != nil { + return ctx, err + } + } + + if err := dfd.DeductFeeAndTip(ctx, tx, fee, tip); err != nil { + return ctx, err + } + + // update fee market state + state, err := dfd.feemarketKeeper.GetState(ctx) + if err != nil { + return ctx, errorsmod.Wrapf(err, "unable to get fee market state") + } + + err = state.Update(gas) + if err != nil { + return ctx, errorsmod.Wrapf(err, "unable to update fee market state") + } + + err = dfd.feemarketKeeper.SetState(ctx, state) + if err != nil { + return ctx, errorsmod.Wrapf(err, "unable to set fee market state") + } + + return next(ctx, tx, simulate, success) +} + +// DeductFeeAndTip deducts the provided fee and tip from the fee payer. +// If the tx uses a feegranter, the fee granter address will pay the fee instead of the tx signer. +func (dfd FeeMarketDeductDecorator) DeductFeeAndTip(ctx sdk.Context, sdkTx sdk.Tx, fee, tip sdk.Coins) error { + feeTx, ok := sdkTx.(sdk.FeeTx) + if !ok { + return errorsmod.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx") + } + + if addr := dfd.accountKeeper.GetModuleAddress(feemarkettypes.FeeCollectorName); addr == nil { + return fmt.Errorf("fee collector module account (%s) has not been set", feemarkettypes.FeeCollectorName) + } + + feePayer := feeTx.FeePayer() + feeGranter := feeTx.FeeGranter() + deductFeesFrom := feePayer + + // if feegranter set deduct fee from feegranter account. + // this works with only when feegrant enabled. + if feeGranter != nil { + if dfd.feegrantKeeper == nil { + return sdkerrors.ErrInvalidRequest.Wrap("fee grants are not enabled") + } else if !feeGranter.Equals(feePayer) { + err := dfd.feegrantKeeper.UseGrantedFees(ctx, feeGranter, feePayer, fee, sdkTx.GetMsgs()) + if err != nil { + return errorsmod.Wrapf(err, "%s does not allow to pay fees for %s", feeGranter, feePayer) + } + } + + deductFeesFrom = feeGranter + } + + deductFeesFromAcc := dfd.accountKeeper.GetAccount(ctx, deductFeesFrom) + if deductFeesFromAcc == nil { + return sdkerrors.ErrUnknownAddress.Wrapf("fee payer address: %s does not exist", deductFeesFrom) + } + + // deduct the fees and tip + if !fee.IsZero() { + err := DeductCoins(dfd.bankKeeper, ctx, deductFeesFromAcc, fee) + if err != nil { + return err + } + } + + if !tip.IsZero() { + err := SendTip(dfd.bankKeeper, ctx, deductFeesFromAcc.GetAddress(), ctx.BlockHeader().ProposerAddress, tip) + if err != nil { + return err + } + } + + events := sdk.Events{ + sdk.NewEvent( + sdk.EventTypeTx, + sdk.NewAttribute(sdk.AttributeKeyFee, fee.String()), + sdk.NewAttribute(sdk.AttributeKeyFeePayer, deductFeesFrom.String()), + sdk.NewAttribute(feemarkettypes.AttributeKeyTip, tip.String()), + sdk.NewAttribute(feemarkettypes.AttributeKeyTipPayer, deductFeesFrom.String()), + ), + } + ctx.EventManager().EmitEvents(events) + + return nil +} + +// DeductCoins deducts coins from the given account. Coins are sent to the feemarket fee collector account. +func DeductCoins(bankKeeper BankKeeper, ctx sdk.Context, acc authtypes.AccountI, coins sdk.Coins) error { + if !coins.IsValid() { + return errorsmod.Wrapf(sdkerrors.ErrInsufficientFee, "invalid coin amount: %s", coins) + } + + err := bankKeeper.SendCoinsFromAccountToModule(ctx, acc.GetAddress(), feemarkettypes.FeeCollectorName, coins) + if err != nil { + return errorsmod.Wrapf(sdkerrors.ErrInsufficientFunds, err.Error()) + } + + return nil +} + +// SendTip sends a tip to the current block proposer. +func SendTip(bankKeeper BankKeeper, ctx sdk.Context, acc, proposer sdk.AccAddress, coins sdk.Coins) error { + if !coins.IsValid() { + return errorsmod.Wrapf(sdkerrors.ErrInsufficientFee, "invalid coin amount: %s", coins) + } + + err := bankKeeper.SendCoins(ctx, acc, proposer, coins) + if err != nil { + return errorsmod.Wrapf(sdkerrors.ErrInsufficientFunds, err.Error()) + } + + return nil +} diff --git a/x/feemarket/post/fee_test.go b/x/feemarket/post/fee_test.go new file mode 100644 index 0000000..17b0e3a --- /dev/null +++ b/x/feemarket/post/fee_test.go @@ -0,0 +1,168 @@ +package post_test + +import ( + "fmt" + "testing" + + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/stretchr/testify/mock" + + sdk "github.com/cosmos/cosmos-sdk/types" + + antesuite "github.com/skip-mev/feemarket/x/feemarket/ante/suite" + "github.com/skip-mev/feemarket/x/feemarket/post" + "github.com/skip-mev/feemarket/x/feemarket/types" +) + +func TestDeductCoins(t *testing.T) { + tests := []struct { + name string + coins sdk.Coins + wantErr bool + invalidCoin bool + }{ + { + name: "valid", + coins: sdk.NewCoins(sdk.NewCoin("test", sdk.NewInt(10))), + wantErr: false, + }, + { + name: "valid no coins", + coins: sdk.NewCoins(), + wantErr: false, + }, + { + name: "invalid coins", + coins: sdk.Coins{sdk.Coin{Amount: sdk.NewInt(-1)}}, + wantErr: true, + invalidCoin: true, + }, + } + for _, tc := range tests { + t.Run(fmt.Sprintf("Case %s", tc.name), func(t *testing.T) { + s := antesuite.SetupTestSuite(t) + acc := s.CreateTestAccounts(1)[0] + if !tc.invalidCoin { + s.BankKeeper.On("SendCoinsFromAccountToModule", s.Ctx, acc.Account.GetAddress(), types.FeeCollectorName, tc.coins).Return(nil).Once() + } + + if err := post.DeductCoins(s.BankKeeper, s.Ctx, acc.Account, tc.coins); (err != nil) != tc.wantErr { + s.Errorf(err, "DeductCoins() error = %v, wantErr %v", err, tc.wantErr) + } + }) + } +} + +func TestSendTip(t *testing.T) { + tests := []struct { + name string + coins sdk.Coins + wantErr bool + invalidCoin bool + }{ + { + name: "valid", + coins: sdk.NewCoins(sdk.NewCoin("test", sdk.NewInt(10))), + wantErr: false, + }, + { + name: "valid no coins", + coins: sdk.NewCoins(), + wantErr: false, + }, + { + name: "invalid coins", + coins: sdk.Coins{sdk.Coin{Amount: sdk.NewInt(-1)}}, + wantErr: true, + invalidCoin: true, + }, + } + for _, tc := range tests { + t.Run(fmt.Sprintf("Case %s", tc.name), func(t *testing.T) { + s := antesuite.SetupTestSuite(t) + accs := s.CreateTestAccounts(2) + if !tc.invalidCoin { + s.BankKeeper.On("SendCoins", s.Ctx, mock.Anything, mock.Anything, tc.coins).Return(nil).Once() + } + + if err := post.SendTip(s.BankKeeper, s.Ctx, accs[0].Account.GetAddress(), accs[1].Account.GetAddress(), tc.coins); (err != nil) != tc.wantErr { + s.Errorf(err, "SendCoins() error = %v, wantErr %v", err, tc.wantErr) + } + }) + } +} + +func TestPostHandle(t *testing.T) { + // Same data for every test case + gasLimit := antesuite.NewTestGasLimit() + validFeeAmount := types.DefaultMinBaseFee.MulRaw(int64(gasLimit)) + validFee := sdk.NewCoins(sdk.NewCoin("stake", validFeeAmount)) + + testCases := []antesuite.TestCase{ + { + Name: "signer has no funds", + Malleate: func(suite *antesuite.TestSuite) antesuite.TestCaseArgs { + accs := suite.CreateTestAccounts(1) + suite.BankKeeper.On("SendCoinsFromAccountToModule", mock.Anything, accs[0].Account.GetAddress(), types.FeeCollectorName, mock.Anything).Return(sdkerrors.ErrInsufficientFunds) + + return antesuite.TestCaseArgs{ + Msgs: []sdk.Msg{testdata.NewTestMsg(accs[0].Account.GetAddress())}, + GasLimit: gasLimit, + FeeAmount: validFee, + } + }, + RunAnte: true, + RunPost: true, + Simulate: false, + ExpPass: false, + ExpErr: sdkerrors.ErrInsufficientFunds, + }, + { + Name: "0 gas given should fail", + Malleate: func(suite *antesuite.TestSuite) antesuite.TestCaseArgs { + accs := suite.CreateTestAccounts(1) + + return antesuite.TestCaseArgs{ + Msgs: []sdk.Msg{testdata.NewTestMsg(accs[0].Account.GetAddress())}, + GasLimit: 0, + FeeAmount: validFee, + } + }, + RunAnte: true, + RunPost: true, + Simulate: false, + ExpPass: false, + ExpErr: sdkerrors.ErrInvalidGasLimit, + }, + { + Name: "signer has enough funds, should pass", + Malleate: func(suite *antesuite.TestSuite) antesuite.TestCaseArgs { + accs := suite.CreateTestAccounts(1) + suite.BankKeeper.On("SendCoinsFromAccountToModule", mock.Anything, accs[0].Account.GetAddress(), types.FeeCollectorName, mock.Anything).Return(nil) + suite.BankKeeper.On("SendCoins", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + + return antesuite.TestCaseArgs{ + Msgs: []sdk.Msg{testdata.NewTestMsg(accs[0].Account.GetAddress())}, + GasLimit: gasLimit, + FeeAmount: validFee, + } + }, + RunAnte: true, + RunPost: true, + Simulate: false, + ExpPass: true, + ExpErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("Case %s", tc.Name), func(t *testing.T) { + s := antesuite.SetupTestSuite(t) + s.TxBuilder = s.ClientCtx.TxConfig.NewTxBuilder() + args := tc.Malleate(s) + + s.RunTestCase(t, tc, args) + }) + } +} diff --git a/x/feemarket/post/mocks/mock_account_keeper.go b/x/feemarket/post/mocks/mock_account_keeper.go new file mode 100644 index 0000000..fb833d3 --- /dev/null +++ b/x/feemarket/post/mocks/mock_account_keeper.go @@ -0,0 +1,113 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + mock "github.com/stretchr/testify/mock" + + types "github.com/cosmos/cosmos-sdk/types" +) + +// AccountKeeper is an autogenerated mock type for the AccountKeeper type +type AccountKeeper struct { + mock.Mock +} + +// GetAccount provides a mock function with given fields: ctx, addr +func (_m *AccountKeeper) GetAccount(ctx types.Context, addr types.AccAddress) authtypes.AccountI { + ret := _m.Called(ctx, addr) + + var r0 authtypes.AccountI + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress) authtypes.AccountI); ok { + r0 = rf(ctx, addr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(authtypes.AccountI) + } + } + + return r0 +} + +// GetModuleAccount provides a mock function with given fields: ctx, name +func (_m *AccountKeeper) GetModuleAccount(ctx types.Context, name string) authtypes.ModuleAccountI { + ret := _m.Called(ctx, name) + + var r0 authtypes.ModuleAccountI + if rf, ok := ret.Get(0).(func(types.Context, string) authtypes.ModuleAccountI); ok { + r0 = rf(ctx, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(authtypes.ModuleAccountI) + } + } + + return r0 +} + +// 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 +} + +// GetParams provides a mock function with given fields: ctx +func (_m *AccountKeeper) GetParams(ctx types.Context) authtypes.Params { + ret := _m.Called(ctx) + + var r0 authtypes.Params + if rf, ok := ret.Get(0).(func(types.Context) authtypes.Params); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(authtypes.Params) + } + + return r0 +} + +// NewAccountWithAddress provides a mock function with given fields: ctx, addr +func (_m *AccountKeeper) NewAccountWithAddress(ctx types.Context, addr types.AccAddress) authtypes.AccountI { + ret := _m.Called(ctx, addr) + + var r0 authtypes.AccountI + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress) authtypes.AccountI); ok { + r0 = rf(ctx, addr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(authtypes.AccountI) + } + } + + return r0 +} + +// SetAccount provides a mock function with given fields: ctx, acc +func (_m *AccountKeeper) SetAccount(ctx types.Context, acc authtypes.AccountI) { + _m.Called(ctx, acc) +} + +// 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/feemarket/post/mocks/mock_bank_keeper.go b/x/feemarket/post/mocks/mock_bank_keeper.go new file mode 100644 index 0000000..26d9807 --- /dev/null +++ b/x/feemarket/post/mocks/mock_bank_keeper.go @@ -0,0 +1,78 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + + types "github.com/cosmos/cosmos-sdk/types" +) + +// BankKeeper is an autogenerated mock type for the BankKeeper type +type BankKeeper struct { + mock.Mock +} + +// IsSendEnabledCoins provides a mock function with given fields: ctx, coins +func (_m *BankKeeper) IsSendEnabledCoins(ctx types.Context, coins ...types.Coin) error { + _va := make([]interface{}, len(coins)) + for _i := range coins { + _va[_i] = coins[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, ...types.Coin) error); ok { + r0 = rf(ctx, coins...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendCoins provides a mock function with given fields: ctx, from, to, amt +func (_m *BankKeeper) SendCoins(ctx types.Context, from types.AccAddress, to types.AccAddress, amt types.Coins) error { + ret := _m.Called(ctx, from, to, amt) + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, types.AccAddress, types.Coins) error); ok { + r0 = rf(ctx, from, to, amt) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendCoinsFromAccountToModule provides a mock function with given fields: ctx, senderAddr, recipientModule, amt +func (_m *BankKeeper) SendCoinsFromAccountToModule(ctx types.Context, senderAddr types.AccAddress, recipientModule string, amt types.Coins) error { + ret := _m.Called(ctx, senderAddr, recipientModule, amt) + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, string, types.Coins) error); ok { + r0 = rf(ctx, senderAddr, recipientModule, 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/feemarket/post/mocks/mock_feegrant_keeper.go b/x/feemarket/post/mocks/mock_feegrant_keeper.go new file mode 100644 index 0000000..aefc832 --- /dev/null +++ b/x/feemarket/post/mocks/mock_feegrant_keeper.go @@ -0,0 +1,43 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + + types "github.com/cosmos/cosmos-sdk/types" +) + +// FeeGrantKeeper is an autogenerated mock type for the FeeGrantKeeper type +type FeeGrantKeeper struct { + mock.Mock +} + +// UseGrantedFees provides a mock function with given fields: ctx, granter, grantee, fee, msgs +func (_m *FeeGrantKeeper) UseGrantedFees(ctx types.Context, granter types.AccAddress, grantee types.AccAddress, fee types.Coins, msgs []types.Msg) error { + ret := _m.Called(ctx, granter, grantee, fee, msgs) + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, types.AccAddress, types.Coins, []types.Msg) error); ok { + r0 = rf(ctx, granter, grantee, fee, msgs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewFeeGrantKeeper creates a new instance of FeeGrantKeeper. 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 NewFeeGrantKeeper(t interface { + mock.TestingT + Cleanup(func()) +}, +) *FeeGrantKeeper { + mock := &FeeGrantKeeper{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/x/feemarket/post/mocks/mock_feemarket_keeper.go b/x/feemarket/post/mocks/mock_feemarket_keeper.go new file mode 100644 index 0000000..ccdd104 --- /dev/null +++ b/x/feemarket/post/mocks/mock_feemarket_keeper.go @@ -0,0 +1,95 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + + feemarkettypes "github.com/skip-mev/feemarket/x/feemarket/types" + + types "github.com/cosmos/cosmos-sdk/types" +) + +// FeeMarketKeeper is an autogenerated mock type for the FeeMarketKeeper type +type FeeMarketKeeper struct { + mock.Mock +} + +// GetMinGasPrices provides a mock function with given fields: ctx +func (_m *FeeMarketKeeper) GetMinGasPrices(ctx types.Context) (types.Coins, error) { + ret := _m.Called(ctx) + + var r0 types.Coins + var r1 error + if rf, ok := ret.Get(0).(func(types.Context) (types.Coins, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(types.Context) types.Coins); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.Coins) + } + } + + if rf, ok := ret.Get(1).(func(types.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetState provides a mock function with given fields: ctx +func (_m *FeeMarketKeeper) GetState(ctx types.Context) (feemarkettypes.State, error) { + ret := _m.Called(ctx) + + var r0 feemarkettypes.State + var r1 error + if rf, ok := ret.Get(0).(func(types.Context) (feemarkettypes.State, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(types.Context) feemarkettypes.State); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(feemarkettypes.State) + } + + if rf, ok := ret.Get(1).(func(types.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SetState provides a mock function with given fields: ctx, state +func (_m *FeeMarketKeeper) SetState(ctx types.Context, state feemarkettypes.State) error { + ret := _m.Called(ctx, state) + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, feemarkettypes.State) error); ok { + r0 = rf(ctx, state) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewFeeMarketKeeper creates a new instance of FeeMarketKeeper. 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 NewFeeMarketKeeper(t interface { + mock.TestingT + Cleanup(func()) +}, +) *FeeMarketKeeper { + mock := &FeeMarketKeeper{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/x/feemarket/types/eip1559.go b/x/feemarket/types/eip1559.go index b2a2360..2d21af3 100644 --- a/x/feemarket/types/eip1559.go +++ b/x/feemarket/types/eip1559.go @@ -38,7 +38,7 @@ var ( // DefaultMinBaseFee is the default minimum base fee. This is the default // on Ethereum. Note that Ethereum is denominated in 1e18 wei. Cosmos chains will // likely want to change this to 1e6. - DefaultMinBaseFee = math.NewInt(1_000_000_000) + DefaultMinBaseFee = math.NewInt(1_000_000) // DefaultMinLearningRate is not used in the base EIP-1559 implementation. DefaultMinLearningRate = math.LegacyMustNewDecFromStr("0.125") diff --git a/x/feemarket/types/expected_keepers.go b/x/feemarket/types/expected_keepers.go new file mode 100644 index 0000000..b455992 --- /dev/null +++ b/x/feemarket/types/expected_keepers.go @@ -0,0 +1,15 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +// AccountKeeper defines the expected account keeper (noalias) +// +//go:generate mockery --name AccountKeeper --filename mock_account_keeper.go +type AccountKeeper interface { + GetAccount(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI + GetModuleAddress(name string) sdk.AccAddress + GetModuleAccount(ctx sdk.Context, name string) authtypes.ModuleAccountI +} diff --git a/x/feemarket/types/keys.go b/x/feemarket/types/keys.go index a0cc483..c2aaf09 100644 --- a/x/feemarket/types/keys.go +++ b/x/feemarket/types/keys.go @@ -5,6 +5,9 @@ const ( ModuleName = "feemarket" // StoreKey is the store key string for the feemarket module. StoreKey = ModuleName + + // FeeCollectorName the root string for the fee market fee collector account address. + FeeCollectorName = "feemarket-fee-collector" ) const ( @@ -18,4 +21,7 @@ var ( // KeyState is the store key for the feemarket module's data. KeyState = []byte{prefixState} + + AttributeKeyTip = "tip" + AttributeKeyTipPayer = "tip_payer" ) diff --git a/x/feemarket/types/mocks/mock_account_keeper.go b/x/feemarket/types/mocks/mock_account_keeper.go new file mode 100644 index 0000000..a4f040a --- /dev/null +++ b/x/feemarket/types/mocks/mock_account_keeper.go @@ -0,0 +1,79 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + mock "github.com/stretchr/testify/mock" + + types "github.com/cosmos/cosmos-sdk/types" +) + +// AccountKeeper is an autogenerated mock type for the AccountKeeper type +type AccountKeeper struct { + mock.Mock +} + +// GetAccount provides a mock function with given fields: ctx, addr +func (_m *AccountKeeper) GetAccount(ctx types.Context, addr types.AccAddress) authtypes.AccountI { + ret := _m.Called(ctx, addr) + + var r0 authtypes.AccountI + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress) authtypes.AccountI); ok { + r0 = rf(ctx, addr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(authtypes.AccountI) + } + } + + return r0 +} + +// GetModuleAccount provides a mock function with given fields: ctx, name +func (_m *AccountKeeper) GetModuleAccount(ctx types.Context, name string) authtypes.ModuleAccountI { + ret := _m.Called(ctx, name) + + var r0 authtypes.ModuleAccountI + if rf, ok := ret.Get(0).(func(types.Context, string) authtypes.ModuleAccountI); ok { + r0 = rf(ctx, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(authtypes.ModuleAccountI) + } + } + + return r0 +} + +// GetModuleAddress provides a mock function with given fields: name +func (_m *AccountKeeper) GetModuleAddress(name string) types.AccAddress { + ret := _m.Called(name) + + var r0 types.AccAddress + if rf, ok := ret.Get(0).(func(string) types.AccAddress); ok { + r0 = rf(name) + } 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 +}