Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make fee deduction from gas charging free #50

Merged
merged 2 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
}

Check warning on line 43 in app/ante/fee.go

View check run for this annotation

Codecov / codecov/patch

app/ante/fee.go#L42-L43

Added lines #L42 - L43 were not covered by tests
beer-1 marked this conversation as resolved.
Show resolved Hide resolved

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")
}
beer-1 marked this conversation as resolved.
Show resolved Hide resolved

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

return extraEIPs, nil
}

func (k Keeper) GetFeeDenom(ctx context.Context) (string, error) {
params, err := k.Params.Get(ctx)
if err != nil {
return "", err
}

Check warning on line 23 in x/evm/keeper/params.go

View check run for this annotation

Codecov / codecov/patch

x/evm/keeper/params.go#L22-L23

Added lines #L22 - L23 were not covered by tests

return params.FeeDenom, nil
beer-1 marked this conversation as resolved.
Show resolved Hide resolved
}
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)
}
Loading