From 88ad9b7e31fd38d0addf6d42cd944516ad21f14d Mon Sep 17 00:00:00 2001 From: beer-1 <147697694+beer-1@users.noreply.github.com> Date: Wed, 21 Aug 2024 17:06:16 +0900 Subject: [PATCH] feat: make fee deduction from gas charging free (#50) * make fee deduction from gas charging * fix test --- app/ante/ante.go | 2 +- app/ante/ante_test.go | 136 ++++++++++++++++++++++++++++++++++++ app/ante/fee.go | 63 +++++++++++++++++ app/ante/fee_test.go | 70 +++++++++++++++++++ app/app.go | 7 -- app/const.go | 3 - x/evm/keeper/params.go | 9 +++ x/evm/keeper/params_test.go | 26 +++++++ 8 files changed, 305 insertions(+), 11 deletions(-) create mode 100644 app/ante/ante_test.go create mode 100644 app/ante/fee.go create mode 100644 app/ante/fee_test.go create mode 100644 x/evm/keeper/params_test.go diff --git a/app/ante/ante.go b/app/ante/ante.go index 58f0d91..df67a9c 100644 --- a/app/ante/ante.go +++ b/app/ante/ante.go @@ -88,7 +88,7 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { ante.NewTxTimeoutHeightDecorator(), ante.NewValidateMemoDecorator(options.AccountKeeper), ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper), - ante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, freeLaneFeeChecker), + NewGasFreeFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.EVMKeeper, freeLaneFeeChecker), // SetPubKeyDecorator must be called before all signature verification decorators ante.NewSetPubKeyDecorator(options.AccountKeeper), ante.NewValidateSigCountDecorator(options.AccountKeeper), diff --git a/app/ante/ante_test.go b/app/ante/ante_test.go new file mode 100644 index 0000000..f00c8df --- /dev/null +++ b/app/ante/ante_test.go @@ -0,0 +1,136 @@ +package ante_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/suite" + + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + + "cosmossdk.io/log" + dbm "github.com/cosmos/cosmos-db" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/server" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsign "github.com/cosmos/cosmos-sdk/x/auth/signing" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + simcli "github.com/cosmos/cosmos-sdk/x/simulation/client/cli" + + minievmapp "github.com/initia-labs/minievm/app" + evmconfig "github.com/initia-labs/minievm/x/evm/config" + evmtypes "github.com/initia-labs/minievm/x/evm/types" +) + +const feeDenom = "feetoken" + +// AnteTestSuite is a test suite to be used with ante handler tests. +type AnteTestSuite struct { + suite.Suite + + app *minievmapp.MinitiaApp + ctx sdk.Context + clientCtx client.Context + txBuilder client.TxBuilder +} + +// returns context and app with params set on account keeper +func (suite *AnteTestSuite) createTestApp(tempDir string) (*minievmapp.MinitiaApp, sdk.Context) { + appOptions := make(simtestutil.AppOptionsMap, 0) + appOptions[flags.FlagHome] = tempDir + appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue + + app := minievmapp.NewMinitiaApp( + log.NewNopLogger(), dbm.NewMemDB(), dbm.NewMemDB(), dbm.NewMemDB(), nil, true, evmconfig.DefaultEVMConfig(), appOptions, + ) + ctx := app.BaseApp.NewUncachedContext(false, tmproto.Header{}) + err := app.AccountKeeper.Params.Set(ctx, authtypes.DefaultParams()) + suite.NoError(err) + + params := evmtypes.DefaultParams() + params.FeeDenom = feeDenom + err = app.EVMKeeper.Params.Set(ctx, params) + suite.NoError(err) + + return app, ctx +} + +// SetupTest setups a new test, with new app, context, and anteHandler. +func (suite *AnteTestSuite) SetupTest() { + tempDir := suite.T().TempDir() + suite.app, suite.ctx = suite.createTestApp(tempDir) + suite.ctx = suite.ctx.WithBlockHeight(1) + + // Set up TxConfig. + encodingConfig := minievmapp.MakeEncodingConfig() + + // We're using TestMsg encoding in some tests, so register it here. + encodingConfig.Amino.RegisterConcrete(&testdata.TestMsg{}, "testdata.TestMsg", nil) + testdata.RegisterInterfaces(encodingConfig.InterfaceRegistry) + + suite.clientCtx = client.Context{}. + WithTxConfig(encodingConfig.TxConfig) +} + +// CreateTestTx is a helper function to create a tx given multiple inputs. +func (suite *AnteTestSuite) CreateTestTx(privs []cryptotypes.PrivKey, accNums []uint64, accSeqs []uint64, chainID string) (authsign.Tx, error) { + defaultSignMode, err := authsign.APISignModeToInternal(suite.clientCtx.TxConfig.SignModeHandler().DefaultMode()) + suite.NoError(err) + + // 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: defaultSignMode, + Signature: nil, + }, + Sequence: accSeqs[i], + } + + sigsV2 = append(sigsV2, sigV2) + } + err = suite.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 := authsign.SignerData{ + Address: sdk.AccAddress(priv.PubKey().Address()).String(), + ChainID: chainID, + AccountNumber: accNums[i], + Sequence: accSeqs[i], + PubKey: priv.PubKey(), + } + sigV2, err := tx.SignWithPrivKey( + context.TODO(), defaultSignMode, signerData, + suite.txBuilder, priv, suite.clientCtx.TxConfig, accSeqs[i]) + if err != nil { + return nil, err + } + + sigsV2 = append(sigsV2, sigV2) + } + err = suite.txBuilder.SetSignatures(sigsV2...) + if err != nil { + return nil, err + } + + return suite.txBuilder.GetTx(), nil +} + +func TestAnteTestSuite(t *testing.T) { + suite.Run(t, new(AnteTestSuite)) +} diff --git a/app/ante/fee.go b/app/ante/fee.go new file mode 100644 index 0000000..e6e1983 --- /dev/null +++ b/app/ante/fee.go @@ -0,0 +1,63 @@ +package ante + +import ( + errorsmod "cosmossdk.io/errors" + storetypes "cosmossdk.io/store/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/auth/ante" + "github.com/cosmos/cosmos-sdk/x/auth/types" + + evmkeeper "github.com/initia-labs/minievm/x/evm/keeper" +) + +// feeDeductionGasAmount is a estimated gas amount of fee payment +const feeDeductionGasAmount = 250_000 + +// GasFreeFeeDecorator is a decorator that sets the gas meter to infinite before calling the inner DeductFeeDecorator +// and then resets the gas meter to the original value after the inner DeductFeeDecorator is called. +// +// This gas meter manipulation only happens when the tx contains a fee which is defined as fee denom in x/evm params. +type GasFreeFeeDecorator struct { + inner ante.DeductFeeDecorator + + // ek is used to get the fee denom from the x/evm params. + ek *evmkeeper.Keeper +} + +func NewGasFreeFeeDecorator( + ak ante.AccountKeeper, bk types.BankKeeper, + fk ante.FeegrantKeeper, ek *evmkeeper.Keeper, + tfc ante.TxFeeChecker) GasFreeFeeDecorator { + return GasFreeFeeDecorator{ + inner: ante.NewDeductFeeDecorator(ak, bk, fk, tfc), + ek: ek, + } +} + +func (fd GasFreeFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + feeTx, ok := tx.(sdk.FeeTx) + if !ok { + return ctx, errorsmod.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx") + } + + fees := feeTx.GetFee() + feeDenom, err := fd.ek.GetFeeDenom(ctx.WithGasMeter(storetypes.NewInfiniteGasMeter())) + if !(err == nil && len(fees) == 1 && fees[0].Denom == feeDenom) { + if simulate && fees.IsZero() { + // Charge gas for fee deduction simulation + // + // At gas simulation normally gas amount is zero, so the gas is not charged in the simulation. + ctx.GasMeter().ConsumeGas(feeDeductionGasAmount, "fee deduction") + } + + return fd.inner.AnteHandle(ctx, tx, simulate, next) + } + + // If the fee contains only one denom and it is the fee denom, set the gas meter to infinite + // to avoid gas consumption for fee deduction. + gasMeter := ctx.GasMeter() + ctx, err = fd.inner.AnteHandle(ctx.WithGasMeter(storetypes.NewInfiniteGasMeter()), tx, simulate, next) + return ctx.WithGasMeter(gasMeter), err +} diff --git a/app/ante/fee_test.go b/app/ante/fee_test.go new file mode 100644 index 0000000..74cb4f7 --- /dev/null +++ b/app/ante/fee_test.go @@ -0,0 +1,70 @@ +package ante_test + +import ( + "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/initia-labs/minievm/app/ante" +) + +func (suite *AnteTestSuite) Test_NotSpendingGasForTxWithFeeDenom() { + suite.SetupTest() // setup + suite.txBuilder = suite.clientCtx.TxConfig.NewTxBuilder() + + feeAnte := ante.NewGasFreeFeeDecorator(suite.app.AccountKeeper, suite.app.BankKeeper, suite.app.FeeGrantKeeper, suite.app.EVMKeeper, nil) + + // keys and addresses + priv1, _, addr1 := testdata.KeyTestPubAddr() + + msg := testdata.NewTestMsg(addr1) + feeAmount := sdk.NewCoins(sdk.NewCoin(feeDenom, math.NewInt(100))) + gasLimit := uint64(200_000) + atomFeeAmount := sdk.NewCoins(sdk.NewCoin("atom", math.NewInt(200))) + + suite.app.EVMKeeper.ERC20Keeper().MintCoins(suite.ctx, addr1, feeAmount.MulInt(math.NewInt(10))) + suite.app.EVMKeeper.ERC20Keeper().MintCoins(suite.ctx, addr1, atomFeeAmount.MulInt(math.NewInt(10))) + + // Case 1. only fee denom + suite.Require().NoError(suite.txBuilder.SetMsgs(msg)) + suite.txBuilder.SetFeeAmount(feeAmount) + suite.txBuilder.SetGasLimit(gasLimit) + + privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0} + tx, err := suite.CreateTestTx(privs, accNums, accSeqs, suite.ctx.ChainID()) + suite.Require().NoError(err) + + gasMeter := storetypes.NewGasMeter(500000) + feeAnte.AnteHandle(suite.ctx.WithGasMeter(gasMeter), tx, false, nil) + suite.Require().Zero(gasMeter.GasConsumed(), "should not consume gas for fee deduction") + + // Case 2. fee denom and other denom + suite.txBuilder.SetFeeAmount(feeAmount.Add(atomFeeAmount...)) + + gasMeter = storetypes.NewGasMeter(500000) + feeAnte.AnteHandle(suite.ctx.WithGasMeter(gasMeter), tx, false, nil) + suite.Require().NotZero(gasMeter.GasConsumed(), "should consume gas for fee deduction") + + // Case 3. other denom + suite.txBuilder.SetFeeAmount(feeAmount.Add(atomFeeAmount...)) + + gasMeter = storetypes.NewGasMeter(500000) + feeAnte.AnteHandle(suite.ctx.WithGasMeter(gasMeter), tx, false, nil) + suite.Require().NotZero(gasMeter.GasConsumed(), "should consume gas for fee deduction") + + // Case 4. no fee + suite.txBuilder.SetFeeAmount(sdk.NewCoins()) + + gasMeter = storetypes.NewGasMeter(500000) + feeAnte.AnteHandle(suite.ctx.WithGasMeter(gasMeter), tx, false, nil) + suite.Require().NotZero(gasMeter.GasConsumed(), "should consume gas for fee deduction") + + // Case 5. simulate gas consumption + suite.txBuilder.SetFeeAmount(sdk.NewCoins()) + + gasMeter = storetypes.NewGasMeter(500000) + feeAnte.AnteHandle(suite.ctx.WithGasMeter(gasMeter), tx, true, nil) + suite.Require().Greater(gasMeter.GasConsumed(), uint64(250000), "should consume gas for fee deduction") +} diff --git a/app/app.go b/app/app.go index 23220d4..f30421a 100644 --- a/app/app.go +++ b/app/app.go @@ -491,13 +491,6 @@ func (app *MinitiaApp) RegisterAPIRoutes(apiSvr *api.Server, apiConfig config.AP } } -// Simulate customize gas simulation to add fee deduction gas amount. -func (app *MinitiaApp) Simulate(txBytes []byte) (sdk.GasInfo, *sdk.Result, error) { - gasInfo, result, err := app.BaseApp.Simulate(txBytes) - gasInfo.GasUsed += FeeDeductionGasAmount - return gasInfo, result, err -} - // RegisterTxService implements the Application.RegisterTxService method. func (app *MinitiaApp) RegisterTxService(clientCtx client.Context) { authtx.RegisterTxService( diff --git a/app/const.go b/app/const.go index ca7e8e4..09fac7e 100644 --- a/app/const.go +++ b/app/const.go @@ -1,9 +1,6 @@ package app const ( - // FeeDeductionGasAmount is a estimated gas amount of fee payment - FeeDeductionGasAmount = 200_000 - // AccountAddressPrefix is the prefix of bech32 encoded address AccountAddressPrefix = "init" diff --git a/x/evm/keeper/params.go b/x/evm/keeper/params.go index 48ddd93..f3057f6 100644 --- a/x/evm/keeper/params.go +++ b/x/evm/keeper/params.go @@ -15,3 +15,12 @@ func (k Keeper) ExtraEIPs(ctx context.Context) ([]int, error) { return extraEIPs, nil } + +func (k Keeper) GetFeeDenom(ctx context.Context) (string, error) { + params, err := k.Params.Get(ctx) + if err != nil { + return "", err + } + + return params.FeeDenom, nil +} diff --git a/x/evm/keeper/params_test.go b/x/evm/keeper/params_test.go new file mode 100644 index 0000000..0ff3f8d --- /dev/null +++ b/x/evm/keeper/params_test.go @@ -0,0 +1,26 @@ +package keeper_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + evmtypes "github.com/initia-labs/minievm/x/evm/types" +) + +func Test_GetFeeDenom(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + denom, err := input.EVMKeeper.GetFeeDenom(ctx) + require.NoError(t, err) + require.Equal(t, evmtypes.DefaultParams().FeeDenom, denom) + + err = input.EVMKeeper.Params.Set(ctx, evmtypes.Params{ + FeeDenom: "eth", + }) + require.NoError(t, err) + + denom, err = input.EVMKeeper.GetFeeDenom(ctx) + require.NoError(t, err) + require.Equal(t, "eth", denom) +}