Skip to content

Commit

Permalink
feat: make fee deduction from gas charging free (#50)
Browse files Browse the repository at this point in the history
* make fee deduction from gas charging

* fix test
  • Loading branch information
beer-1 authored Aug 21, 2024
1 parent 7dd9388 commit 88ad9b7
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 11 deletions.
2 changes: 1 addition & 1 deletion app/ante/ante.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
136 changes: 136 additions & 0 deletions app/ante/ante_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
63 changes: 63 additions & 0 deletions app/ante/fee.go
Original file line number Diff line number Diff line change
@@ -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
}
70 changes: 70 additions & 0 deletions app/ante/fee_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
7 changes: 0 additions & 7 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 0 additions & 3 deletions app/const.go
Original file line number Diff line number Diff line change
@@ -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"

Expand Down
9 changes: 9 additions & 0 deletions x/evm/keeper/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
26 changes: 26 additions & 0 deletions x/evm/keeper/params_test.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 88ad9b7

Please sign in to comment.