From 43c3c478b2eec25675c59358eceb1b46ba4c846a Mon Sep 17 00:00:00 2001 From: MalteHerrmann Date: Wed, 7 Aug 2024 18:44:52 +0200 Subject: [PATCH] wip adding ante and adjusting example chain --- ante/cosmos/authz.go | 91 ++++ ante/cosmos/authz_test.go | 463 ++++++++++++++++ ante/cosmos/min_gas_price.go | 98 ++++ ante/cosmos/min_gas_price_test.go | 202 +++++++ ante/cosmos/reject_msgs.go | 29 + ante/cosmos/setup_test.go | 151 ++++++ ante/cosmos/utils_test.go | 128 +++++ ante/evm/01_setup_ctx.go | 52 ++ ante/evm/02_mempool_fee.go | 29 + ante/evm/02_mempool_fee_test.go | 65 +++ ante/evm/03_global_fee.go | 38 ++ ante/evm/03_global_fee_test.go | 60 +++ ante/evm/04_validate.go | 115 ++++ ante/evm/04_validate_test.go | 216 ++++++++ ante/evm/05_signature_verification.go | 85 +++ ante/evm/06_account_verification.go | 48 ++ ante/evm/06_account_verification_test.go | 137 +++++ ante/evm/07_can_transfer.go | 61 +++ ante/evm/07_can_transfer_test.go | 105 ++++ ante/evm/08_gas_consume.go | 150 ++++++ ante/evm/09_gas_consume_test.go | 194 +++++++ ante/evm/09_increment_sequence.go | 41 ++ ante/evm/10_gas_wanted.go | 84 +++ ante/evm/10_incremenet_sequence_test.go | 72 +++ ante/evm/11_emit_event.go | 60 +++ ante/evm/11_gas_wanted_test.go | 120 +++++ ante/evm/fee_checker.go | 161 ++++++ ante/evm/fee_checker_test.go | 243 +++++++++ ante/evm/setup_test.go | 132 +++++ ante/evm/suite_test.go | 30 ++ ante/evm/utils.go | 77 +++ ante/evm/utils_test.go | 650 +++++++++++++++++++++++ ante/interfaces/cosmos.go | 28 + ante/interfaces/evm.go | 50 ++ ante/sigverify.go | 90 ++++ ante/sigverify_test.go | 115 ++++ ante/utils_test.go | 38 ++ example_chain/ante/ante.go | 55 ++ example_chain/ante/cosmos_handler.go | 39 ++ example_chain/ante/evm_handler.go | 281 ++++++++++ example_chain/ante/handler_options.go | 71 +++ example_chain/app.go | 9 +- example_chain/eth_test_helpers.go | 197 +++++++ example_chain/export.go | 8 + example_chain/test_helpers.go | 8 +- example_chain/testutil/abci.go | 260 +++++++++ example_chain/testutil/contract.go | 164 ++++++ example_chain/testutil/fund.go | 41 ++ testutil/ante.go | 17 + testutil/tx/cosmos.go | 14 +- testutil/tx/eip712.go | 18 +- testutil/tx/eth.go | 16 +- 52 files changed, 5675 insertions(+), 31 deletions(-) create mode 100644 ante/cosmos/authz.go create mode 100644 ante/cosmos/authz_test.go create mode 100644 ante/cosmos/min_gas_price.go create mode 100644 ante/cosmos/min_gas_price_test.go create mode 100644 ante/cosmos/reject_msgs.go create mode 100644 ante/cosmos/setup_test.go create mode 100644 ante/cosmos/utils_test.go create mode 100644 ante/evm/01_setup_ctx.go create mode 100644 ante/evm/02_mempool_fee.go create mode 100644 ante/evm/02_mempool_fee_test.go create mode 100644 ante/evm/03_global_fee.go create mode 100644 ante/evm/03_global_fee_test.go create mode 100644 ante/evm/04_validate.go create mode 100644 ante/evm/04_validate_test.go create mode 100644 ante/evm/05_signature_verification.go create mode 100644 ante/evm/06_account_verification.go create mode 100644 ante/evm/06_account_verification_test.go create mode 100644 ante/evm/07_can_transfer.go create mode 100644 ante/evm/07_can_transfer_test.go create mode 100644 ante/evm/08_gas_consume.go create mode 100644 ante/evm/09_gas_consume_test.go create mode 100644 ante/evm/09_increment_sequence.go create mode 100644 ante/evm/10_gas_wanted.go create mode 100644 ante/evm/10_incremenet_sequence_test.go create mode 100644 ante/evm/11_emit_event.go create mode 100644 ante/evm/11_gas_wanted_test.go create mode 100644 ante/evm/fee_checker.go create mode 100644 ante/evm/fee_checker_test.go create mode 100644 ante/evm/setup_test.go create mode 100644 ante/evm/suite_test.go create mode 100644 ante/evm/utils.go create mode 100644 ante/evm/utils_test.go create mode 100644 ante/interfaces/cosmos.go create mode 100644 ante/interfaces/evm.go create mode 100644 ante/sigverify.go create mode 100644 ante/sigverify_test.go create mode 100644 ante/utils_test.go create mode 100644 example_chain/ante/ante.go create mode 100644 example_chain/ante/cosmos_handler.go create mode 100644 example_chain/ante/evm_handler.go create mode 100644 example_chain/ante/handler_options.go create mode 100644 example_chain/eth_test_helpers.go create mode 100644 example_chain/testutil/abci.go create mode 100644 example_chain/testutil/contract.go create mode 100644 example_chain/testutil/fund.go create mode 100644 testutil/ante.go diff --git a/ante/cosmos/authz.go b/ante/cosmos/authz.go new file mode 100644 index 0000000..069384a --- /dev/null +++ b/ante/cosmos/authz.go @@ -0,0 +1,91 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package cosmos + +import ( + "fmt" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/authz" +) + +// maxNestedMsgs defines a cap for the number of nested messages on a MsgExec message +const maxNestedMsgs = 7 + +// AuthzLimiterDecorator blocks certain msg types from being granted or executed +// within the authorization module. +type AuthzLimiterDecorator struct { + // disabledMsgTypes is the type urls of the msgs to block. + disabledMsgTypes []string +} + +// NewAuthzLimiterDecorator creates a decorator to block certain msg types from being granted or executed within authz. +func NewAuthzLimiterDecorator(disabledMsgTypes ...string) AuthzLimiterDecorator { + return AuthzLimiterDecorator{ + disabledMsgTypes: disabledMsgTypes, + } +} + +func (ald AuthzLimiterDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + if err := ald.checkDisabledMsgs(tx.GetMsgs(), false, 1); err != nil { + return ctx, errorsmod.Wrapf(errortypes.ErrUnauthorized, err.Error()) + } + return next(ctx, tx, simulate) +} + +// checkDisabledMsgs iterates through the msgs and returns an error if it finds any unauthorized msgs. +// +// When searchOnlyInAuthzMsgs is enabled, only authz MsgGrant and MsgExec are blocked, if they contain unauthorized msg types. +// Otherwise, any msg matching the disabled types are blocked, regardless of being in an authz msg or not. +// +// This method is recursive as MsgExec's can wrap other MsgExecs. The check for nested messages is performed up to the +// maxNestedMsgs threshold. If there are more than that limit, it returns an error +func (ald AuthzLimiterDecorator) checkDisabledMsgs(msgs []sdk.Msg, isAuthzInnerMsg bool, nestedLvl int) error { + if nestedLvl >= maxNestedMsgs { + return fmt.Errorf("found more nested msgs than permited. Limit is : %d", maxNestedMsgs) + } + for _, msg := range msgs { + switch msg := msg.(type) { + case *authz.MsgExec: + innerMsgs, err := msg.GetMessages() + if err != nil { + return err + } + nestedLvl++ + if err := ald.checkDisabledMsgs(innerMsgs, true, nestedLvl); err != nil { + return err + } + case *authz.MsgGrant: + authorization, err := msg.GetAuthorization() + if err != nil { + return err + } + + url := authorization.MsgTypeURL() + if ald.isDisabledMsg(url) { + return fmt.Errorf("found disabled msg type: %s", url) + } + default: + url := sdk.MsgTypeURL(msg) + if isAuthzInnerMsg && ald.isDisabledMsg(url) { + return fmt.Errorf("found disabled msg type: %s", url) + } + } + } + return nil +} + +// isDisabledMsg returns true if the given message is in the list of restricted +// messages from the AnteHandler. +func (ald AuthzLimiterDecorator) isDisabledMsg(msgTypeURL string) bool { + for _, disabledType := range ald.disabledMsgTypes { + if msgTypeURL == disabledType { + return true + } + } + + return false +} diff --git a/ante/cosmos/authz_test.go b/ante/cosmos/authz_test.go new file mode 100644 index 0000000..8cf9da4 --- /dev/null +++ b/ante/cosmos/authz_test.go @@ -0,0 +1,463 @@ +package cosmos_test + +import ( + "fmt" + "math/big" + "testing" + "time" + + abci "github.com/cometbft/cometbft/abci/types" + "github.com/stretchr/testify/require" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + sdkvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + ethtypes "github.com/ethereum/go-ethereum/core/types" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + cosmosante "github.com/evmos/os/ante/cosmos" + "github.com/evmos/os/testutil" + utiltx "github.com/evmos/os/testutil/tx" +) + +func TestAuthzLimiterDecorator(t *testing.T) { + testPrivKeys, testAddresses, err := generatePrivKeyAddressPairs(5) + require.NoError(t, err) + + distantFuture := time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC) + + validator := sdk.ValAddress(testAddresses[4]) + stakingAuthDelegate, err := stakingtypes.NewStakeAuthorization([]sdk.ValAddress{validator}, nil, stakingtypes.AuthorizationType_AUTHORIZATION_TYPE_DELEGATE, nil) + require.NoError(t, err) + + stakingAuthUndelegate, err := stakingtypes.NewStakeAuthorization([]sdk.ValAddress{validator}, nil, stakingtypes.AuthorizationType_AUTHORIZATION_TYPE_UNDELEGATE, nil) + require.NoError(t, err) + + decorator := cosmosante.NewAuthzLimiterDecorator( + sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}), + sdk.MsgTypeURL(&stakingtypes.MsgUndelegate{}), + ) + + testCases := []struct { + name string + msgs []sdk.Msg + checkTx bool + expectedErr error + }{ + { + "enabled msg - non blocked msg", + []sdk.Msg{ + banktypes.NewMsgSend( + testAddresses[0], + testAddresses[1], + sdk.NewCoins(sdk.NewInt64Coin(evmtypes.DefaultEVMDenom, 100e6)), + ), + }, + false, + nil, + }, + { + "enabled msg MsgEthereumTx - blocked msg not wrapped in MsgExec", + []sdk.Msg{ + &evmtypes.MsgEthereumTx{}, + }, + false, + nil, + }, + { + "enabled msg - blocked msg not wrapped in MsgExec", + []sdk.Msg{ + &stakingtypes.MsgCancelUnbondingDelegation{}, + }, + false, + nil, + }, + { + "enabled msg - MsgGrant contains a non blocked msg", + []sdk.Msg{ + newMsgGrant( + testAddresses[0], + testAddresses[1], + authz.NewGenericAuthorization(sdk.MsgTypeURL(&banktypes.MsgSend{})), + &distantFuture, + ), + }, + false, + nil, + }, + { + "enabled msg - MsgGrant contains a non blocked msg", + []sdk.Msg{ + newMsgGrant( + testAddresses[0], + testAddresses[1], + stakingAuthDelegate, + &distantFuture, + ), + }, + false, + nil, + }, + { + "disabled msg - MsgGrant contains a blocked msg", + []sdk.Msg{ + newMsgGrant( + testAddresses[0], + testAddresses[1], + authz.NewGenericAuthorization(sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{})), + &distantFuture, + ), + }, + false, + sdkerrors.ErrUnauthorized, + }, + { + "disabled msg - MsgGrant contains a blocked msg", + []sdk.Msg{ + newMsgGrant( + testAddresses[0], + testAddresses[1], + stakingAuthUndelegate, + &distantFuture, + ), + }, + false, + sdkerrors.ErrUnauthorized, + }, + { + "allowed msg - when a MsgExec contains a non blocked msg", + []sdk.Msg{ + newMsgExec( + testAddresses[1], + []sdk.Msg{banktypes.NewMsgSend( + testAddresses[0], + testAddresses[3], + sdk.NewCoins(sdk.NewInt64Coin(evmtypes.DefaultEVMDenom, 100e6)), + )}), + }, + false, + nil, + }, + { + "disabled msg - MsgExec contains a blocked msg", + []sdk.Msg{ + newMsgExec( + testAddresses[1], + []sdk.Msg{ + &evmtypes.MsgEthereumTx{}, + }, + ), + }, + false, + sdkerrors.ErrUnauthorized, + }, + { + "disabled msg - surrounded by valid msgs", + []sdk.Msg{ + newMsgGrant( + testAddresses[0], + testAddresses[1], + stakingAuthDelegate, + &distantFuture, + ), + newMsgExec( + testAddresses[1], + []sdk.Msg{ + banktypes.NewMsgSend( + testAddresses[0], + testAddresses[3], + sdk.NewCoins(sdk.NewInt64Coin(evmtypes.DefaultEVMDenom, 100e6)), + ), + &evmtypes.MsgEthereumTx{}, + }, + ), + }, + false, + sdkerrors.ErrUnauthorized, + }, + { + "disabled msg - nested MsgExec containing a blocked msg", + []sdk.Msg{ + createNestedMsgExec( + testAddresses[1], + 2, + []sdk.Msg{ + &evmtypes.MsgEthereumTx{}, + }, + ), + }, + false, + sdkerrors.ErrUnauthorized, + }, + { + "disabled msg - nested MsgGrant containing a blocked msg", + []sdk.Msg{ + newMsgExec( + testAddresses[1], + []sdk.Msg{ + newMsgGrant( + testAddresses[0], + testAddresses[1], + authz.NewGenericAuthorization(sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{})), + &distantFuture, + ), + }, + ), + }, + false, + sdkerrors.ErrUnauthorized, + }, + { + "disabled msg - nested MsgExec NOT containing a blocked msg but has more nesting levels than the allowed", + []sdk.Msg{ + createNestedMsgExec( + testAddresses[1], + 6, + []sdk.Msg{ + banktypes.NewMsgSend( + testAddresses[0], + testAddresses[3], + sdk.NewCoins(sdk.NewInt64Coin(evmtypes.DefaultEVMDenom, 100e6)), + ), + }, + ), + }, + false, + sdkerrors.ErrUnauthorized, + }, + { + "disabled msg - multiple two nested MsgExec messages NOT containing a blocked msg over the limit", + []sdk.Msg{ + createNestedMsgExec( + testAddresses[1], + 5, + []sdk.Msg{ + banktypes.NewMsgSend( + testAddresses[0], + testAddresses[3], + sdk.NewCoins(sdk.NewInt64Coin(evmtypes.DefaultEVMDenom, 100e6)), + ), + }, + ), + createNestedMsgExec( + testAddresses[1], + 5, + []sdk.Msg{ + banktypes.NewMsgSend( + testAddresses[0], + testAddresses[3], + sdk.NewCoins(sdk.NewInt64Coin(evmtypes.DefaultEVMDenom, 100e6)), + ), + }, + ), + }, + false, + sdkerrors.ErrUnauthorized, + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("Case %s", tc.name), func(t *testing.T) { + ctx := sdk.Context{}.WithIsCheckTx(tc.checkTx) + tx, err := createTx(testPrivKeys[0], tc.msgs...) + require.NoError(t, err) + + _, err = decorator.AnteHandle(ctx, tx, false, testutil.NoOpNextFn) + if tc.expectedErr != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedErr) + } else { + require.NoError(t, err) + } + }) + } +} + +func (suite *AnteTestSuite) TestRejectMsgsInAuthz() { + _, testAddresses, err := generatePrivKeyAddressPairs(10) + suite.Require().NoError(err) + + distantFuture := time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC) + + // create a dummy MsgEthereumTx for the test + // otherwise throws error that cannot unpack tx data + msgEthereumTx := evmtypes.NewTx(&evmtypes.EvmTxArgs{ + ChainID: big.NewInt(9000), + Nonce: 0, + GasLimit: 1000000, + GasFeeCap: suite.app.FeeMarketKeeper.GetBaseFee(suite.ctx), + GasTipCap: big.NewInt(1), + Input: nil, + Accesses: ðtypes.AccessList{}, + }) + + newMsgGrant := func(msgTypeUrl string) *authz.MsgGrant { + msg, err := authz.NewMsgGrant( + testAddresses[0], + testAddresses[1], + authz.NewGenericAuthorization(msgTypeUrl), + &distantFuture, + ) + if err != nil { + panic(err) + } + return msg + } + + testcases := []struct { + name string + msgs []sdk.Msg + expectedCode uint32 + isEIP712 bool + }{ + { + name: "a MsgGrant with MsgEthereumTx typeURL on the authorization field is blocked", + msgs: []sdk.Msg{newMsgGrant(sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}))}, + expectedCode: sdkerrors.ErrUnauthorized.ABCICode(), + }, + { + name: "a MsgGrant with MsgCreateVestingAccount typeURL on the authorization field is blocked", + msgs: []sdk.Msg{newMsgGrant(sdk.MsgTypeURL(&sdkvesting.MsgCreateVestingAccount{}))}, + expectedCode: sdkerrors.ErrUnauthorized.ABCICode(), + }, + { + name: "a MsgGrant with MsgEthereumTx typeURL on the authorization field included on EIP712 tx is blocked", + msgs: []sdk.Msg{newMsgGrant(sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}))}, + expectedCode: sdkerrors.ErrUnauthorized.ABCICode(), + isEIP712: true, + }, + { + name: "a MsgExec with nested messages (valid: MsgSend and invalid: MsgEthereumTx) is blocked", + msgs: []sdk.Msg{ + newMsgExec( + testAddresses[1], + []sdk.Msg{ + banktypes.NewMsgSend( + testAddresses[0], + testAddresses[3], + sdk.NewCoins(sdk.NewInt64Coin(evmtypes.DefaultEVMDenom, 100e6)), + ), + msgEthereumTx, + }, + ), + }, + expectedCode: sdkerrors.ErrUnauthorized.ABCICode(), + }, + { + name: "a MsgExec with nested MsgExec messages that has invalid messages is blocked", + msgs: []sdk.Msg{ + createNestedMsgExec( + testAddresses[1], + 2, + []sdk.Msg{ + msgEthereumTx, + }, + ), + }, + expectedCode: sdkerrors.ErrUnauthorized.ABCICode(), + }, + { + name: "a MsgExec with more nested MsgExec messages than allowed and with valid messages is blocked", + msgs: []sdk.Msg{ + createNestedMsgExec( + testAddresses[1], + 6, + []sdk.Msg{ + banktypes.NewMsgSend( + testAddresses[0], + testAddresses[3], + sdk.NewCoins(sdk.NewInt64Coin(evmtypes.DefaultEVMDenom, 100e6)), + ), + }, + ), + }, + expectedCode: sdkerrors.ErrUnauthorized.ABCICode(), + }, + { + name: "two MsgExec messages NOT containing a blocked msg but between the two have more nesting than the allowed. Then, is blocked", + msgs: []sdk.Msg{ + createNestedMsgExec( + testAddresses[1], + 5, + []sdk.Msg{ + banktypes.NewMsgSend( + testAddresses[0], + testAddresses[3], + sdk.NewCoins(sdk.NewInt64Coin(evmtypes.DefaultEVMDenom, 100e6)), + ), + }, + ), + createNestedMsgExec( + testAddresses[1], + 5, + []sdk.Msg{ + banktypes.NewMsgSend( + testAddresses[0], + testAddresses[3], + sdk.NewCoins(sdk.NewInt64Coin(evmtypes.DefaultEVMDenom, 100e6)), + ), + }, + ), + }, + expectedCode: sdkerrors.ErrUnauthorized.ABCICode(), + }, + } + + for _, tc := range testcases { + suite.Run(fmt.Sprintf("Case %s", tc.name), func() { + suite.SetupTest() + var ( + tx sdk.Tx + err error + ) + + if tc.isEIP712 { + coinAmount := sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(20)) + fees := sdk.NewCoins(coinAmount) + cosmosTxArgs := utiltx.CosmosTxArgs{ + TxCfg: suite.clientCtx.TxConfig, + Priv: suite.priv, + ChainID: suite.ctx.ChainID(), + Gas: 200000, + Fees: fees, + Msgs: tc.msgs, + } + + tx, err = utiltx.CreateEIP712CosmosTx( + suite.ctx, + suite.app, + utiltx.EIP712TxArgs{ + CosmosTxArgs: cosmosTxArgs, + UseLegacyTypedData: true, + }, + ) + } else { + tx, err = createTx(suite.priv, tc.msgs...) + } + suite.Require().NoError(err) + + txEncoder := suite.clientCtx.TxConfig.TxEncoder() + bz, err := txEncoder(tx) + suite.Require().NoError(err) + + resCheckTx := suite.app.CheckTx( + abci.RequestCheckTx{ + Tx: bz, + Type: abci.CheckTxType_New, + }, + ) + suite.Require().Equal(resCheckTx.Code, tc.expectedCode, resCheckTx.Log) + + resDeliverTx := suite.app.DeliverTx( + abci.RequestDeliverTx{ + Tx: bz, + }, + ) + suite.Require().Equal(resDeliverTx.Code, tc.expectedCode, resDeliverTx.Log) + }) + } +} diff --git a/ante/cosmos/min_gas_price.go b/ante/cosmos/min_gas_price.go new file mode 100644 index 0000000..8a2784b --- /dev/null +++ b/ante/cosmos/min_gas_price.go @@ -0,0 +1,98 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package cosmos + +import ( + "fmt" + "math/big" + "slices" + + errorsmod "cosmossdk.io/errors" + "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + anteinterfaces "github.com/evmos/os/ante/interfaces" +) + +// MinGasPriceDecorator will check if the transaction's fee is at least as large +// as the MinGasPrices param. If fee is too low, decorator returns error and tx +// is rejected. This applies for both CheckTx and DeliverTx +// If fee is high enough, then call next AnteHandler +// CONTRACT: Tx must implement FeeTx to use MinGasPriceDecorator +type MinGasPriceDecorator struct { + feesKeeper anteinterfaces.FeeMarketKeeper + evmKeeper anteinterfaces.EVMKeeper +} + +// NewMinGasPriceDecorator creates a new MinGasPriceDecorator instance used only for +// Cosmos transactions. +func NewMinGasPriceDecorator(fk anteinterfaces.FeeMarketKeeper, ek anteinterfaces.EVMKeeper) MinGasPriceDecorator { + return MinGasPriceDecorator{feesKeeper: fk, evmKeeper: ek} +} + +func (mpd MinGasPriceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + feeTx, ok := tx.(sdk.FeeTx) + if !ok { + return ctx, errorsmod.Wrapf(errortypes.ErrInvalidType, "invalid transaction type %T, expected sdk.FeeTx", tx) + } + + minGasPrice := mpd.feesKeeper.GetParams(ctx).MinGasPrice + + feeCoins := feeTx.GetFee() + evmParams := mpd.evmKeeper.GetParams(ctx) + evmDenom := evmParams.GetEvmDenom() + + // only allow user to pass in aevmos and stake native token as transaction fees + // allow use stake native tokens for fees is just for unit tests to pass + // + // TODO: is the handling of stake necessary here? Why not adjust the tests to contain the correct denom? + validFees := len(feeCoins) == 0 || (len(feeCoins) == 1 && slices.Contains([]string{evmDenom, sdk.DefaultBondDenom}, feeCoins.GetDenomByIndex(0))) + if !validFees && !simulate { + return ctx, fmt.Errorf("expected only native token %s for fee, but got %s", evmDenom, feeCoins.String()) + } + + // Short-circuit if min gas price is 0 or if simulating + if minGasPrice.IsZero() || simulate { + return next(ctx, tx, simulate) + } + + minGasPrices := sdk.DecCoins{ + { + Denom: evmDenom, + Amount: minGasPrice, + }, + } + + gas := feeTx.GetGas() + + requiredFees := make(sdk.Coins, 0) + + // Determine the required fees by multiplying each required minimum gas + // price by the gas limit, where fee = ceil(minGasPrice * gasLimit). + gasLimit := math.LegacyNewDecFromBigInt(new(big.Int).SetUint64(gas)) + + for _, gp := range minGasPrices { + fee := gp.Amount.Mul(gasLimit).Ceil().RoundInt() + if fee.IsPositive() { + requiredFees = requiredFees.Add(sdk.Coin{Denom: gp.Denom, Amount: fee}) + } + } + + // Fees not provided (or flag "auto"). Then use the base fee to make the check pass + if feeCoins == nil { + return ctx, errorsmod.Wrapf(errortypes.ErrInsufficientFee, + "fee not provided. Please use the --fees flag or the --gas-price flag along with the --gas flag to estimate the fee. The minimum global fee for this tx is: %s", + requiredFees) + } + + if !feeCoins.IsAnyGTE(requiredFees) { + return ctx, errorsmod.Wrapf(errortypes.ErrInsufficientFee, + "provided fee < minimum global fee (%s < %s). Please increase the gas price.", + feeCoins, + requiredFees) + } + + return next(ctx, tx, simulate) +} diff --git a/ante/cosmos/min_gas_price_test.go b/ante/cosmos/min_gas_price_test.go new file mode 100644 index 0000000..1d236b6 --- /dev/null +++ b/ante/cosmos/min_gas_price_test.go @@ -0,0 +1,202 @@ +package cosmos_test + +import ( + "fmt" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + cosmosante "github.com/evmos/evmos/v19/app/ante/cosmos" + "github.com/evmos/os/testutil" + testutiltx "github.com/evmos/os/testutil/tx" +) + +var execTypes = []struct { + name string + isCheckTx bool + simulate bool +}{ + {"deliverTx", false, false}, + {"deliverTxSimulate", false, true}, +} + +func (suite *AnteTestSuite) TestMinGasPriceDecorator() { + denom := testutil.ExampleAttoDenom + testMsg := banktypes.MsgSend{ + FromAddress: "evmos1x8fhpj9nmhqk8z9kpgjt95ck2xwyue0ptzkucp", + ToAddress: "evmos1dx67l23hz9l0k9hcher8xz04uj7wf3yu26l2yn", + Amount: sdk.Coins{sdk.Coin{Amount: math.NewInt(10), Denom: denom}}, + } + + testCases := []struct { + name string + malleate func() sdk.Tx + expPass bool + errMsg string + allowPassOnSimulate bool + }{ + { + "invalid cosmos tx type", + func() sdk.Tx { + return &testutiltx.InvalidTx{} + }, + false, + "invalid transaction type", + false, + }, + { + "valid cosmos tx with MinGasPrices = 0, gasPrice = 0", + func() sdk.Tx { + params := suite.app.FeeMarketKeeper.GetParams(suite.ctx) + params.MinGasPrice = math.LegacyZeroDec() + err := suite.app.FeeMarketKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + + txBuilder := suite.CreateTestCosmosTxBuilder(math.NewInt(0), denom, &testMsg) + return txBuilder.GetTx() + }, + true, + "", + true, + }, + { + "valid cosmos tx with MinGasPrices = 0, gasPrice > 0", + func() sdk.Tx { + params := suite.app.FeeMarketKeeper.GetParams(suite.ctx) + params.MinGasPrice = math.LegacyZeroDec() + err := suite.app.FeeMarketKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + + txBuilder := suite.CreateTestCosmosTxBuilder(math.NewInt(10), denom, &testMsg) + return txBuilder.GetTx() + }, + true, + "", + true, + }, + { + "valid cosmos tx with MinGasPrices = 10, gasPrice = 10", + func() sdk.Tx { + params := suite.app.FeeMarketKeeper.GetParams(suite.ctx) + params.MinGasPrice = math.LegacyNewDec(10) + err := suite.app.FeeMarketKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + + txBuilder := suite.CreateTestCosmosTxBuilder(math.NewInt(10), denom, &testMsg) + return txBuilder.GetTx() + }, + true, + "", + true, + }, + { + "invalid cosmos tx with MinGasPrices = 10, gasPrice = 0", + func() sdk.Tx { + params := suite.app.FeeMarketKeeper.GetParams(suite.ctx) + params.MinGasPrice = math.LegacyNewDec(10) + err := suite.app.FeeMarketKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + + txBuilder := suite.CreateTestCosmosTxBuilder(math.NewInt(0), denom, &testMsg) + return txBuilder.GetTx() + }, + false, + "provided fee < minimum global fee", + true, + }, + { + "invalid cosmos tx with stake denom", + func() sdk.Tx { + params := suite.app.FeeMarketKeeper.GetParams(suite.ctx) + params.MinGasPrice = math.LegacyNewDec(10) + err := suite.app.FeeMarketKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + + txBuilder := suite.CreateTestCosmosTxBuilder(math.NewInt(10), sdk.DefaultBondDenom, &testMsg) + return txBuilder.GetTx() + }, + false, + "provided fee < minimum global fee", + true, + }, + { + "valid cosmos tx with MinGasPrices = 0, gasPrice = 0, valid fee", + func() sdk.Tx { + params := suite.app.FeeMarketKeeper.GetParams(suite.ctx) + params.MinGasPrice = math.LegacyZeroDec() + err := suite.app.FeeMarketKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + + txBuilder := suite.CreateTestCosmosTxBuilderWithFees(sdk.Coins{sdk.Coin{Amount: math.NewInt(0), Denom: denom}}, &testMsg) + return txBuilder.GetTx() + }, + true, + "", + true, + }, + { + "valid cosmos tx with MinGasPrices = 0, gasPrice = 0, nil fees, means len(fees) == 0", + func() sdk.Tx { + params := suite.app.FeeMarketKeeper.GetParams(suite.ctx) + params.MinGasPrice = math.LegacyZeroDec() + err := suite.app.FeeMarketKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + + txBuilder := suite.CreateTestCosmosTxBuilderWithFees(nil, &testMsg) + return txBuilder.GetTx() + }, + true, + "", + true, + }, + { + "valid cosmos tx with MinGasPrices = 0, gasPrice = 0, empty fees, means len(fees) == 0", + func() sdk.Tx { + params := suite.app.FeeMarketKeeper.GetParams(suite.ctx) + params.MinGasPrice = math.LegacyZeroDec() + err := suite.app.FeeMarketKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + + txBuilder := suite.CreateTestCosmosTxBuilderWithFees(sdk.Coins{}, &testMsg) + return txBuilder.GetTx() + }, + true, + "", + true, + }, + { + "valid cosmos tx with MinGasPrices = 0, gasPrice = 0, invalid fees", + func() sdk.Tx { + params := suite.app.FeeMarketKeeper.GetParams(suite.ctx) + params.MinGasPrice = math.LegacyZeroDec() + err := suite.app.FeeMarketKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + + fees := sdk.Coins{sdk.Coin{Amount: math.NewInt(0), Denom: denom}, sdk.Coin{Amount: math.NewInt(10), Denom: "stake"}} + txBuilder := suite.CreateTestCosmosTxBuilderWithFees(fees, &testMsg) + return txBuilder.GetTx() + }, + false, + fmt.Sprintf("expected only use native token %s for fee", denom), + true, + }, + } + + for _, et := range execTypes { + for _, tc := range testCases { + suite.Run(et.name+"_"+tc.name, func() { + // s.SetupTest(et.isCheckTx) + ctx := suite.ctx.WithIsReCheckTx(et.isCheckTx) + dec := cosmosante.NewMinGasPriceDecorator(suite.app.FeeMarketKeeper, suite.app.EVMKeeper) + _, err := dec.AnteHandle(ctx, tc.malleate(), et.simulate, testutil.NoOpNextFn) + + if (et.name == "deliverTx" && tc.expPass) || (et.name == "deliverTxSimulate" && et.simulate && tc.allowPassOnSimulate) { + suite.Require().NoError(err, tc.name) + } else { + suite.Require().Error(err, tc.name) + suite.Require().Contains(err.Error(), tc.errMsg, tc.name) + } + }) + } + } +} diff --git a/ante/cosmos/reject_msgs.go b/ante/cosmos/reject_msgs.go new file mode 100644 index 0000000..5bdebe1 --- /dev/null +++ b/ante/cosmos/reject_msgs.go @@ -0,0 +1,29 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package cosmos + +import ( + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" +) + +// RejectMessagesDecorator prevents invalid msg types from being executed +type RejectMessagesDecorator struct{} + +// AnteHandle rejects messages that requires ethereum-specific authentication. +// For example `MsgEthereumTx` requires fee to be deducted in the antehandler in +// order to perform the refund. +func (rmd RejectMessagesDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + for _, msg := range tx.GetMsgs() { + if _, ok := msg.(*evmtypes.MsgEthereumTx); ok { + return ctx, errorsmod.Wrapf( + errortypes.ErrInvalidType, + "MsgEthereumTx needs to be contained within a tx with 'ExtensionOptionsEthereumTx' option", + ) + } + } + return next(ctx, tx, simulate) +} diff --git a/ante/cosmos/setup_test.go b/ante/cosmos/setup_test.go new file mode 100644 index 0000000..47d45b2 --- /dev/null +++ b/ante/cosmos/setup_test.go @@ -0,0 +1,151 @@ +package cosmos_test + +import ( + "math" + "testing" + "time" + + "github.com/stretchr/testify/suite" + + sdkmath "cosmossdk.io/math" + "cosmossdk.io/simapp" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/cosmos/cosmos-sdk/client" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/evmos/evmos/v19/x/evm/statedb" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + feemarkettypes "github.com/evmos/evmos/v19/x/feemarket/types" + "github.com/evmos/os/ante" + evmante "github.com/evmos/os/ante/evm" + "github.com/evmos/os/crypto/ethsecp256k1" + "github.com/evmos/os/encoding" + "github.com/evmos/os/ethereum/eip712" + app "github.com/evmos/os/example_chain" + chainante "github.com/evmos/os/example_chain/ante" + chaintestutil "github.com/evmos/os/example_chain/testutil" + "github.com/evmos/os/testutil" + "github.com/evmos/os/types" +) + +type AnteTestSuite struct { + suite.Suite + + ctx sdk.Context + app *app.ExampleChain + clientCtx client.Context + anteHandler sdk.AnteHandler + ethSigner ethtypes.Signer + priv cryptotypes.PrivKey + enableFeemarket bool + enableLondonHF bool + evmParamsOption func(*evmtypes.Params) +} + +const TestGasLimit uint64 = 100000 + +var chainID = testutil.ExampleChainID + +func (suite *AnteTestSuite) StateDB() *statedb.StateDB { + return statedb.New(suite.ctx, suite.app.EVMKeeper, statedb.NewEmptyTxConfig(common.BytesToHash(suite.ctx.HeaderHash().Bytes()))) +} + +func (suite *AnteTestSuite) SetupTest() { + checkTx := false + priv, err := ethsecp256k1.GenerateKey() + suite.Require().NoError(err) + suite.priv = priv + + suite.app = app.EthSetup(checkTx, func(app *app.ExampleChain, genesis simapp.GenesisState) simapp.GenesisState { + if suite.enableFeemarket { + // setup feemarketGenesis params + feemarketGenesis := feemarkettypes.DefaultGenesisState() + feemarketGenesis.Params.EnableHeight = 1 + feemarketGenesis.Params.NoBaseFee = false + // Verify feeMarket genesis + err := feemarketGenesis.Validate() + suite.Require().NoError(err) + genesis[feemarkettypes.ModuleName] = app.AppCodec().MustMarshalJSON(feemarketGenesis) + } + evmGenesis := evmtypes.DefaultGenesisState() + evmGenesis.Params.AllowUnprotectedTxs = false + if !suite.enableLondonHF { + maxInt := sdkmath.NewInt(math.MaxInt64) + evmGenesis.Params.ChainConfig.LondonBlock = &maxInt + evmGenesis.Params.ChainConfig.ArrowGlacierBlock = &maxInt + evmGenesis.Params.ChainConfig.GrayGlacierBlock = &maxInt + evmGenesis.Params.ChainConfig.MergeNetsplitBlock = &maxInt + evmGenesis.Params.ChainConfig.ShanghaiBlock = &maxInt + evmGenesis.Params.ChainConfig.CancunBlock = &maxInt + } + if suite.evmParamsOption != nil { + suite.evmParamsOption(&evmGenesis.Params) + } + genesis[evmtypes.ModuleName] = app.AppCodec().MustMarshalJSON(evmGenesis) + return genesis + }) + + suite.ctx = suite.app.BaseApp.NewContext(checkTx, tmproto.Header{Height: 2, ChainID: chainID, Time: time.Now().UTC()}) + suite.ctx = suite.ctx.WithMinGasPrices(sdk.NewDecCoins(sdk.NewDecCoin(testutil.ExampleAttoDenom, sdkmath.OneInt()))) + suite.ctx = suite.ctx.WithBlockGasMeter(storetypes.NewGasMeter(1000000000000000000)) + + stakingParams := suite.app.StakingKeeper.GetParams(suite.ctx) + stakingParams.BondDenom = testutil.ExampleAttoDenom + err = suite.app.StakingKeeper.SetParams(suite.ctx, stakingParams) + suite.Require().NoError(err) + + infCtx := suite.ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + err = suite.app.AccountKeeper.SetParams(infCtx, authtypes.DefaultParams()) + suite.Require().NoError(err) + + encodingConfig := encoding.MakeConfig(app.ModuleBasics) + // We're using TestMsg amino encoding in some tests, so register it here. + encodingConfig.Amino.RegisterConcrete(&testdata.TestMsg{}, "testdata.TestMsg", nil) + eip712.SetEncodingConfig(encodingConfig) + + suite.clientCtx = client.Context{}.WithTxConfig(encodingConfig.TxConfig) + + anteHandler := chainante.NewAnteHandler(chainante.HandlerOptions{ + AccountKeeper: suite.app.AccountKeeper, + BankKeeper: suite.app.BankKeeper, + EvmKeeper: suite.app.EVMKeeper, + FeegrantKeeper: suite.app.FeeGrantKeeper, + StakingKeeper: suite.app.StakingKeeper, + FeeMarketKeeper: suite.app.FeeMarketKeeper, + SignModeHandler: encodingConfig.TxConfig.SignModeHandler(), + SigGasConsumer: ante.SigVerificationGasConsumer, + ExtensionOptionChecker: types.HasDynamicFeeExtensionOption, + TxFeeChecker: evmante.NewDynamicFeeChecker(suite.app.EVMKeeper), + }) + + suite.anteHandler = anteHandler + suite.ethSigner = ethtypes.LatestSignerForChainID(suite.app.EVMKeeper.ChainID()) + + // fund signer acc to pay for tx fees + amt := sdkmath.NewInt(int64(math.Pow10(18) * 2)) + err = chaintestutil.FundAccount( + suite.ctx, + suite.app.BankKeeper, + suite.priv.PubKey().Address().Bytes(), + sdk.NewCoins(sdk.NewCoin(testutil.ExampleAttoDenom, amt)), + ) + suite.Require().NoError(err) + + header := suite.ctx.BlockHeader() + suite.ctx = suite.ctx.WithBlockHeight(header.Height - 1) + suite.ctx, err = chaintestutil.Commit(suite.ctx, suite.app, time.Second*0, nil) + suite.Require().NoError(err) +} + +func TestAnteTestSuite(t *testing.T) { + suite.Run(t, &AnteTestSuite{ + enableLondonHF: true, + }) +} diff --git a/ante/cosmos/utils_test.go b/ante/cosmos/utils_test.go new file mode 100644 index 0000000..7efce24 --- /dev/null +++ b/ante/cosmos/utils_test.go @@ -0,0 +1,128 @@ +package cosmos_test + +import ( + "time" + + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/cosmos/cosmos-sdk/x/authz" + "github.com/evmos/evmos/os/encoding" + "github.com/evmos/evmos/v19/app" + "github.com/evmos/os/crypto/ethsecp256k1" +) + +func (suite *AnteTestSuite) CreateTestCosmosTxBuilder(gasPrice sdkmath.Int, denom string, msgs ...sdk.Msg) client.TxBuilder { + txBuilder := suite.clientCtx.TxConfig.NewTxBuilder() + + txBuilder.SetGasLimit(TestGasLimit) + fees := &sdk.Coins{{Denom: denom, Amount: gasPrice.MulRaw(int64(TestGasLimit))}} + txBuilder.SetFeeAmount(*fees) + err := txBuilder.SetMsgs(msgs...) + suite.Require().NoError(err) + return txBuilder +} + +func (suite *AnteTestSuite) CreateTestCosmosTxBuilderWithFees(fees sdk.Coins, msgs ...sdk.Msg) client.TxBuilder { + txBuilder := suite.clientCtx.TxConfig.NewTxBuilder() + txBuilder.SetGasLimit(TestGasLimit) + txBuilder.SetFeeAmount(fees) + err := txBuilder.SetMsgs(msgs...) + suite.Require().NoError(err) + return txBuilder +} + +func newMsgExec(grantee sdk.AccAddress, msgs []sdk.Msg) *authz.MsgExec { + msg := authz.NewMsgExec(grantee, msgs) + return &msg +} + +func newMsgGrant(granter sdk.AccAddress, grantee sdk.AccAddress, a authz.Authorization, expiration *time.Time) *authz.MsgGrant { + msg, err := authz.NewMsgGrant(granter, grantee, a, expiration) + if err != nil { + panic(err) + } + return msg +} + +func createNestedMsgExec(a sdk.AccAddress, nestedLvl int, lastLvlMsgs []sdk.Msg) *authz.MsgExec { + msgs := make([]*authz.MsgExec, nestedLvl) + for i := range msgs { + if i == 0 { + msgs[i] = newMsgExec(a, lastLvlMsgs) + continue + } + msgs[i] = newMsgExec(a, []sdk.Msg{msgs[i-1]}) + } + return msgs[nestedLvl-1] +} + +func generatePrivKeyAddressPairs(accCount int) ([]*ethsecp256k1.PrivKey, []sdk.AccAddress, error) { + var ( + err error + testPrivKeys = make([]*ethsecp256k1.PrivKey, accCount) + testAddresses = make([]sdk.AccAddress, accCount) + ) + + for i := range testPrivKeys { + testPrivKeys[i], err = ethsecp256k1.GenerateKey() + if err != nil { + return nil, nil, err + } + testAddresses[i] = testPrivKeys[i].PubKey().Address().Bytes() + } + return testPrivKeys, testAddresses, nil +} + +func createTx(priv cryptotypes.PrivKey, msgs ...sdk.Msg) (sdk.Tx, error) { + encodingConfig := encoding.MakeConfig(app.ModuleBasics) + txBuilder := encodingConfig.TxConfig.NewTxBuilder() + + txBuilder.SetGasLimit(1000000) + if err := txBuilder.SetMsgs(msgs...); err != nil { + return nil, err + } + + // First round: we gather all the signer infos. We use the "set empty + // signature" hack to do that. + sigV2 := signing.SignatureV2{ + PubKey: priv.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: encodingConfig.TxConfig.SignModeHandler().DefaultMode(), + Signature: nil, + }, + Sequence: 0, + } + + sigsV2 := []signing.SignatureV2{sigV2} + + if err := txBuilder.SetSignatures(sigsV2...); err != nil { + return nil, err + } + + signerData := authsigning.SignerData{ + ChainID: chainID, + AccountNumber: 0, + Sequence: 0, + } + sigV2, err := tx.SignWithPrivKey( + encodingConfig.TxConfig.SignModeHandler().DefaultMode(), signerData, + txBuilder, priv, encodingConfig.TxConfig, + 0, + ) + if err != nil { + return nil, err + } + + sigsV2 = []signing.SignatureV2{sigV2} + err = txBuilder.SetSignatures(sigsV2...) + if err != nil { + return nil, err + } + + return txBuilder.GetTx(), nil +} diff --git a/ante/evm/01_setup_ctx.go b/ante/evm/01_setup_ctx.go new file mode 100644 index 0000000..9ef5651 --- /dev/null +++ b/ante/evm/01_setup_ctx.go @@ -0,0 +1,52 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package evm + +import ( + errorsmod "cosmossdk.io/errors" + sdktypes "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + evmante "github.com/evmos/evmos/v19/x/evm/ante" + anteinterfaces "github.com/evmos/os/ante/interfaces" +) + +var _ sdktypes.AnteDecorator = &EthSetupContextDecorator{} + +// EthSetupContextDecorator is adapted from SetUpContextDecorator from cosmos-sdk, it ignores gas consumption +// by setting the gas meter to infinite +type EthSetupContextDecorator struct { + evmKeeper anteinterfaces.EVMKeeper +} + +func NewEthSetUpContextDecorator(evmKeeper anteinterfaces.EVMKeeper) EthSetupContextDecorator { + return EthSetupContextDecorator{ + evmKeeper: evmKeeper, + } +} + +func (esc EthSetupContextDecorator) AnteHandle(ctx sdktypes.Context, tx sdktypes.Tx, simulate bool, next sdktypes.AnteHandler) (newCtx sdktypes.Context, err error) { + newCtx, err = SetupContext(ctx, tx, esc.evmKeeper) + if err != nil { + return ctx, err + } + return next(newCtx, tx, simulate) +} + +func SetupContext(ctx sdktypes.Context, tx sdktypes.Tx, evmKeeper anteinterfaces.EVMKeeper) (sdktypes.Context, error) { + // all transactions must implement GasTx + _, ok := tx.(authante.GasTx) + if !ok { + return ctx, errorsmod.Wrapf(errortypes.ErrInvalidType, "invalid transaction type %T, expected GasTx", tx) + } + + // We need to set up an empty gas config so that the gas is consistent with Ethereum. + newCtx := evmante.BuildEvmExecutionCtx(ctx). + WithGasMeter(sdktypes.NewInfiniteGasMeter()) + // Reset transient gas used to prepare the execution of current cosmos tx. + // Transient gas-used is necessary to sum the gas-used of cosmos tx, when it contains multiple eth msgs. + evmKeeper.ResetTransientGasUsed(ctx) + + return newCtx, nil +} diff --git a/ante/evm/02_mempool_fee.go b/ante/evm/02_mempool_fee.go new file mode 100644 index 0000000..43e7a3b --- /dev/null +++ b/ante/evm/02_mempool_fee.go @@ -0,0 +1,29 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package evm + +import ( + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" +) + +// CheckMempoolFee checks if the provided fee is at least as large as the local validator's +func CheckMempoolFee(fee, mempoolMinGasPrice, gasLimit sdkmath.LegacyDec, isLondon bool) error { + if isLondon { + return nil + } + + requiredFee := mempoolMinGasPrice.Mul(gasLimit) + + if fee.LT(requiredFee) { + return errorsmod.Wrapf( + errortypes.ErrInsufficientFee, + "insufficient fee; got: %s required: %s", + fee, requiredFee, + ) + } + + return nil +} diff --git a/ante/evm/02_mempool_fee_test.go b/ante/evm/02_mempool_fee_test.go new file mode 100644 index 0000000..48b2ee9 --- /dev/null +++ b/ante/evm/02_mempool_fee_test.go @@ -0,0 +1,65 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package evm_test + +import ( + sdkmath "cosmossdk.io/math" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/evmos/os/ante/evm" +) + +func (suite *EvmAnteTestSuite) TestMempoolFee() { + testCases := []struct { + name string + expectedError error + isLondon bool + txFee sdkmath.LegacyDec + minGasPrice sdkmath.LegacyDec + gasLimit sdkmath.LegacyDec + }{ + { + name: "success: if London fork is enabled, skip check", + expectedError: nil, + isLondon: true, + // values are not used because isLondon is true + txFee: sdkmath.LegacyOneDec(), + minGasPrice: sdkmath.LegacyOneDec(), + gasLimit: sdkmath.LegacyOneDec(), + }, + { + name: "success: fee is greater than min gas price * gas limit", + expectedError: nil, + isLondon: false, + txFee: sdkmath.LegacyNewDec(100), + minGasPrice: sdkmath.LegacyOneDec(), + gasLimit: sdkmath.LegacyOneDec(), + }, + { + name: "fail: fee is less than min gas price * gas limit", + expectedError: errortypes.ErrInsufficientFee, + isLondon: false, + txFee: sdkmath.LegacyOneDec(), + minGasPrice: sdkmath.LegacyNewDec(100), + gasLimit: sdkmath.LegacyOneDec(), + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + // Function under test + err := evm.CheckMempoolFee( + tc.txFee, + tc.minGasPrice, + tc.gasLimit, + tc.isLondon, + ) + + if tc.expectedError != nil { + suite.Require().Error(err) + suite.Contains(err.Error(), tc.expectedError.Error()) + } else { + suite.Require().NoError(err) + } + }) + } +} diff --git a/ante/evm/03_global_fee.go b/ante/evm/03_global_fee.go new file mode 100644 index 0000000..2f815d5 --- /dev/null +++ b/ante/evm/03_global_fee.go @@ -0,0 +1,38 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package evm + +import ( + errorsmod "cosmossdk.io/errors" + "cosmossdk.io/math" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" +) + +// CheckGlobalFee validates the provided fee value against the required global fee. +// +// For dynamic transactions, GetFee() uses the GasFeeCap value, which +// is the maximum gas price that the signer can pay. In practice, the +// signer can pay less, if the block's BaseFee is lower. So, in this case, +// we use the EffectiveFee. If the feemarket formula results in a BaseFee +// that lowers EffectivePrice until it is < MinGasPrices, the users must +// increase the GasTipCap (priority fee) until EffectivePrice > MinGasPrices. +// Transactions with MinGasPrices * gasUsed < tx fees < EffectiveFee are rejected +// by the feemarket AnteHandle +func CheckGlobalFee(fee, globalMinGasPrice, gasLimit math.LegacyDec) error { + if globalMinGasPrice.IsZero() { + return nil + } + + requiredFee := globalMinGasPrice.Mul(gasLimit) + + if fee.LT(requiredFee) { + return errorsmod.Wrapf( + errortypes.ErrInsufficientFee, + "provided fee < minimum global fee (%s < %s). Please increase the priority tip (for EIP-1559 txs) or the gas prices (for access list or legacy txs)", //nolint:lll + fee.TruncateInt().String(), requiredFee.TruncateInt().String(), + ) + } + + return nil +} diff --git a/ante/evm/03_global_fee_test.go b/ante/evm/03_global_fee_test.go new file mode 100644 index 0000000..9231261 --- /dev/null +++ b/ante/evm/03_global_fee_test.go @@ -0,0 +1,60 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package evm_test + +import ( + sdkmath "cosmossdk.io/math" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/evmos/os/ante/evm" +) + +func (suite *EvmAnteTestSuite) TestGlobalFee() { + testCases := []struct { + name string + expectedError error + txFee sdkmath.LegacyDec + globalMinGasPrice sdkmath.LegacyDec + gasLimit sdkmath.LegacyDec + }{ + { + name: "success: if globalMinGasPrice is 0, skip check", + expectedError: nil, + // values are not used because isLondon is true + txFee: sdkmath.LegacyOneDec(), + globalMinGasPrice: sdkmath.LegacyZeroDec(), + gasLimit: sdkmath.LegacyOneDec(), + }, + { + name: "success: fee is greater than global gas price * gas limit", + expectedError: nil, + txFee: sdkmath.LegacyNewDec(100), + globalMinGasPrice: sdkmath.LegacyOneDec(), + gasLimit: sdkmath.LegacyOneDec(), + }, + { + name: "fail: fee is less than global gas price * gas limit", + expectedError: errortypes.ErrInsufficientFee, + txFee: sdkmath.LegacyOneDec(), + globalMinGasPrice: sdkmath.LegacyNewDec(100), + gasLimit: sdkmath.LegacyOneDec(), + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + // Function under test + err := evm.CheckGlobalFee( + tc.txFee, + tc.globalMinGasPrice, + tc.gasLimit, + ) + + if tc.expectedError != nil { + suite.Require().Error(err) + suite.Contains(err.Error(), tc.expectedError.Error()) + } else { + suite.Require().NoError(err) + } + }) + } +} diff --git a/ante/evm/04_validate.go b/ante/evm/04_validate.go new file mode 100644 index 0000000..859546d --- /dev/null +++ b/ante/evm/04_validate.go @@ -0,0 +1,115 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package evm + +import ( + "errors" + + errorsmod "cosmossdk.io/errors" + sdktypes "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/tx" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + anteinterfaces "github.com/evmos/os/ante/interfaces" +) + +// ValidateMsg validates an Ethereum specific message type and returns an error if invalid +// +// It checks the following requirements: +// - nil MUST be passed as the from address +// - If the transaction is a contract creation or call, the corresponding operation must be enabled in the EVM parameters +func ValidateMsg( + evmParams evmtypes.Params, + txData evmtypes.TxData, + from sdktypes.AccAddress, +) error { + if from != nil { + return errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid from address; expected nil; got: %q", from.String()) + } + + return checkDisabledCreateCall( + txData, + &evmParams.AccessControl, + ) +} + +// checkDisabledCreateCall checks if the transaction is a contract creation or call, +// and if those actions are disabled through governance. +func checkDisabledCreateCall( + txData evmtypes.TxData, + permissions *evmtypes.AccessControl, +) error { + to := txData.GetTo() + blockCreate := permissions.Create.AccessType == evmtypes.AccessTypeRestricted + blockCall := permissions.Call.AccessType == evmtypes.AccessTypeRestricted + + // return error if contract creation or call are disabled through governance + // and the transaction is trying to create a contract or call a contract + if blockCreate && to == nil { + return errorsmod.Wrap(evmtypes.ErrCreateDisabled, "failed to create new contract") + } else if blockCall && to != nil { + return errorsmod.Wrap(evmtypes.ErrCallDisabled, "failed to perform a call") + } + return nil +} + +// ValidateTx validates an Ethereum specific transaction type and returns an error if invalid. +// +// FIXME: this shouldn't be required if the tx was an Ethereum transaction type. +func ValidateTx(tx sdktypes.Tx) (*tx.Fee, error) { + err := tx.ValidateBasic() + // ErrNoSignatures is fine with eth tx + if err != nil && !errors.Is(err, errortypes.ErrNoSignatures) { + return nil, errorsmod.Wrap(err, "tx basic validation failed") + } + + // For eth type cosmos tx, some fields should be verified as zero values, + // since we will only verify the signature against the hash of the MsgEthereumTx.Data + wrapperTx, ok := tx.(anteinterfaces.ProtoTxProvider) + if !ok { + return nil, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid tx type %T, didn't implement interface ProtoTxProvider", tx) + } + + protoTx := wrapperTx.GetProtoTx() + body := protoTx.Body + if body.Memo != "" || body.TimeoutHeight != uint64(0) || len(body.NonCriticalExtensionOptions) > 0 { + return nil, errorsmod.Wrap(errortypes.ErrInvalidRequest, + "for eth tx body Memo TimeoutHeight NonCriticalExtensionOptions should be empty") + } + + if len(body.ExtensionOptions) != 1 { + return nil, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx length of ExtensionOptions should be 1") + } + + authInfo := protoTx.AuthInfo + if len(authInfo.SignerInfos) > 0 { + return nil, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx AuthInfo SignerInfos should be empty") + } + + if authInfo.Fee.Payer != "" || authInfo.Fee.Granter != "" { + return nil, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx AuthInfo Fee payer and granter should be empty") + } + + sigs := protoTx.Signatures + if len(sigs) > 0 { + return nil, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx Signatures should be empty") + } + + return authInfo.Fee, nil +} + +func CheckTxFee(txFeeInfo *tx.Fee, txFee sdktypes.Coins, txGasLimit uint64) error { + if txFeeInfo == nil { + return nil + } + + if !txFeeInfo.Amount.IsEqual(txFee) { + return errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid AuthInfo Fee Amount (%s != %s)", txFeeInfo.Amount, txFee) + } + + if txFeeInfo.GasLimit != txGasLimit { + return errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid AuthInfo Fee GasLimit (%d != %d)", txFeeInfo.GasLimit, txGasLimit) + } + + return nil +} diff --git a/ante/evm/04_validate_test.go b/ante/evm/04_validate_test.go new file mode 100644 index 0000000..6686ec6 --- /dev/null +++ b/ante/evm/04_validate_test.go @@ -0,0 +1,216 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package evm_test + +import ( + "math/big" + + sdktypes "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/ethereum/go-ethereum/common" + testkeyring "github.com/evmos/evmos/v19/testutil/integration/evmos/keyring" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + "github.com/evmos/os/ante/evm" +) + +type validateMsgParams struct { + evmParams evmtypes.Params + from sdktypes.AccAddress + txData evmtypes.TxData +} + +func (suite *EvmAnteTestSuite) TestValidateMsg() { + keyring := testkeyring.New(2) + + testCases := []struct { + name string + expectedError error + getFunctionParams func() validateMsgParams + }{ + { + name: "fail: invalid from address, should be nil", + expectedError: errortypes.ErrInvalidRequest, + getFunctionParams: func() validateMsgParams { + return validateMsgParams{ + evmParams: evmtypes.DefaultParams(), + txData: nil, + from: keyring.GetAccAddr(0), + } + }, + }, + { + name: "success: transfer with default params", + expectedError: nil, + getFunctionParams: func() validateMsgParams { + txArgs := getTxByType("transfer", keyring.GetAddr(1)) + txData, err := txArgs.ToTxData() + suite.Require().NoError(err) + return validateMsgParams{ + evmParams: evmtypes.DefaultParams(), + txData: txData, + from: nil, + } + }, + }, + { + name: "success: transfer with disable call and create", + expectedError: evmtypes.ErrCallDisabled, + getFunctionParams: func() validateMsgParams { + txArgs := getTxByType("transfer", keyring.GetAddr(1)) + txData, err := txArgs.ToTxData() + suite.Require().NoError(err) + + params := evmtypes.DefaultParams() + params.AccessControl.Call.AccessType = evmtypes.AccessTypeRestricted + params.AccessControl.Create.AccessType = evmtypes.AccessTypeRestricted + + return validateMsgParams{ + evmParams: params, + txData: txData, + from: nil, + } + }, + }, + { + name: "success: call with default params", + expectedError: nil, + getFunctionParams: func() validateMsgParams { + txArgs := getTxByType("call", keyring.GetAddr(1)) + txData, err := txArgs.ToTxData() + suite.Require().NoError(err) + return validateMsgParams{ + evmParams: evmtypes.DefaultParams(), + txData: txData, + from: nil, + } + }, + }, + { + name: "success: call tx with disabled create", + expectedError: nil, + getFunctionParams: func() validateMsgParams { + txArgs := getTxByType("call", keyring.GetAddr(1)) + txData, err := txArgs.ToTxData() + suite.Require().NoError(err) + + params := evmtypes.DefaultParams() + params.AccessControl.Create.AccessType = evmtypes.AccessTypeRestricted + + return validateMsgParams{ + evmParams: params, + txData: txData, + from: nil, + } + }, + }, + { + name: "fail: call tx with disabled call", + expectedError: evmtypes.ErrCallDisabled, + getFunctionParams: func() validateMsgParams { + txArgs := getTxByType("call", keyring.GetAddr(1)) + txData, err := txArgs.ToTxData() + suite.Require().NoError(err) + + params := evmtypes.DefaultParams() + params.AccessControl.Call.AccessType = evmtypes.AccessTypeRestricted + + return validateMsgParams{ + evmParams: params, + txData: txData, + from: nil, + } + }, + }, + { + name: "success: create with default params", + expectedError: nil, + getFunctionParams: func() validateMsgParams { + txArgs := getTxByType("create", keyring.GetAddr(1)) + txData, err := txArgs.ToTxData() + suite.Require().NoError(err) + return validateMsgParams{ + evmParams: evmtypes.DefaultParams(), + txData: txData, + from: nil, + } + }, + }, + { + name: "success: create with disable call", + expectedError: nil, + getFunctionParams: func() validateMsgParams { + txArgs := getTxByType("create", keyring.GetAddr(1)) + txData, err := txArgs.ToTxData() + suite.Require().NoError(err) + + params := evmtypes.DefaultParams() + params.AccessControl.Call.AccessType = evmtypes.AccessTypeRestricted + + return validateMsgParams{ + evmParams: params, + txData: txData, + from: nil, + } + }, + }, + { + name: "fail: create with disable create", + expectedError: evmtypes.ErrCreateDisabled, + getFunctionParams: func() validateMsgParams { + txArgs := getTxByType("create", keyring.GetAddr(1)) + txData, err := txArgs.ToTxData() + suite.Require().NoError(err) + + params := evmtypes.DefaultParams() + params.AccessControl.Create.AccessType = evmtypes.AccessTypeRestricted + + return validateMsgParams{ + evmParams: params, + txData: txData, + from: nil, + } + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + params := tc.getFunctionParams() + + // Function under test + err := evm.ValidateMsg( + params.evmParams, + params.txData, + params.from, + ) + + if tc.expectedError != nil { + suite.Require().Error(err) + suite.Contains(err.Error(), tc.expectedError.Error()) + } else { + suite.Require().NoError(err) + } + }) + } +} + +func getTxByType(typeTx string, recipient common.Address) evmtypes.EvmTxArgs { + switch typeTx { + case "call": + return evmtypes.EvmTxArgs{ + To: &recipient, + Input: []byte("call bytes"), + } + case "create": + return evmtypes.EvmTxArgs{ + Input: []byte("create bytes"), + } + case "transfer": + return evmtypes.EvmTxArgs{ + To: &recipient, + Amount: big.NewInt(100), + } + default: + panic("invalid type") + } +} diff --git a/ante/evm/05_signature_verification.go b/ante/evm/05_signature_verification.go new file mode 100644 index 0000000..26d4c4b --- /dev/null +++ b/ante/evm/05_signature_verification.go @@ -0,0 +1,85 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package evm + +import ( + "math/big" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + ethtypes "github.com/ethereum/go-ethereum/core/types" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + anteinterfaces "github.com/evmos/os/ante/interfaces" +) + +// EthSigVerificationDecorator validates an ethereum signatures +type EthSigVerificationDecorator struct { + evmKeeper anteinterfaces.EVMKeeper +} + +// NewEthSigVerificationDecorator creates a new EthSigVerificationDecorator +func NewEthSigVerificationDecorator(ek anteinterfaces.EVMKeeper) EthSigVerificationDecorator { + return EthSigVerificationDecorator{ + evmKeeper: ek, + } +} + +// AnteHandle validates checks that the registered chain id is the same as the one on the message, and +// that the signer address matches the one defined on the message. +// It's not skipped for RecheckTx, because it set `From` address which is critical from other ante handler to work. +// Failure in RecheckTx will prevent tx to be included into block, especially when CheckTx succeed, in which case user +// won't see the error message. +func (esvd EthSigVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + chainID := esvd.evmKeeper.ChainID() + evmParams := esvd.evmKeeper.GetParams(ctx) + chainCfg := evmParams.GetChainConfig() + ethCfg := chainCfg.EthereumConfig(chainID) + blockNum := big.NewInt(ctx.BlockHeight()) + signer := ethtypes.MakeSigner(ethCfg, blockNum) + allowUnprotectedTxs := evmParams.GetAllowUnprotectedTxs() + + for _, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evmtypes.MsgEthereumTx)(nil)) + } + + err := SignatureVerification(msgEthTx, signer, allowUnprotectedTxs) + if err != nil { + return ctx, err + } + } + + return next(ctx, tx, simulate) +} + +// SignatureVerification checks that the registered chain id is the same as the one on the message, and +// that the signer address matches the one defined on the message. +func SignatureVerification( + msg *evmtypes.MsgEthereumTx, + signer ethtypes.Signer, + allowUnprotectedTxs bool, +) error { + ethTx := msg.AsTransaction() + + if !allowUnprotectedTxs && !ethTx.Protected() { + return errorsmod.Wrapf( + errortypes.ErrNotSupported, + "rejected unprotected Ethereum transaction. Please EIP155 sign your transaction to protect it against replay-attacks") + } + + sender, err := signer.Sender(ethTx) + if err != nil { + return errorsmod.Wrapf( + errortypes.ErrorInvalidSigner, + "couldn't retrieve sender address from the ethereum transaction: %s", + err.Error(), + ) + } + + // set up the sender to the transaction field if not already + msg.From = sender.Hex() + return nil +} diff --git a/ante/evm/06_account_verification.go b/ante/evm/06_account_verification.go new file mode 100644 index 0000000..904877e --- /dev/null +++ b/ante/evm/06_account_verification.go @@ -0,0 +1,48 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package evm + +import ( + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/ethereum/go-ethereum/common" + "github.com/evmos/evmos/v19/x/evm/keeper" + "github.com/evmos/evmos/v19/x/evm/statedb" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" +) + +// VerifyAccountBalance checks that the account balance is greater than the total transaction cost. +// The account will be set to store if it doesn't exist, i.e. cannot be found on store. +// This method will fail if: +// - from address is NOT an EOA +// - account balance is lower than the transaction cost +func VerifyAccountBalance( + ctx sdk.Context, + accountKeeper evmtypes.AccountKeeper, + account *statedb.Account, + from common.Address, + txData evmtypes.TxData, +) error { + // check whether the sender address is EOA + if account != nil && account.IsContract() { + return errorsmod.Wrapf( + errortypes.ErrInvalidType, + "the sender is not EOA: address %s", from, + ) + } + + if account == nil { + acc := accountKeeper.NewAccountWithAddress(ctx, from.Bytes()) + accountKeeper.SetAccount(ctx, acc) + account = statedb.NewEmptyAccount() + } + + if err := keeper.CheckSenderBalance(sdkmath.NewIntFromBigInt(account.Balance), txData); err != nil { + return errorsmod.Wrap(err, "failed to check sender balance") + } + + return nil +} diff --git a/ante/evm/06_account_verification_test.go b/ante/evm/06_account_verification_test.go new file mode 100644 index 0000000..bbfe551 --- /dev/null +++ b/ante/evm/06_account_verification_test.go @@ -0,0 +1,137 @@ +package evm_test + +import ( + "fmt" + "math/big" + + "cosmossdk.io/math" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/ethereum/go-ethereum/common" + "github.com/evmos/evmos/v19/testutil/integration/evmos/factory" + "github.com/evmos/evmos/v19/testutil/integration/evmos/grpc" + testkeyring "github.com/evmos/evmos/v19/testutil/integration/evmos/keyring" + "github.com/evmos/evmos/v19/testutil/integration/evmos/network" + "github.com/evmos/evmos/v19/x/evm/statedb" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + "github.com/evmos/os/ante/evm" +) + +func (suite *EvmAnteTestSuite) TestVerifyAccountBalance() { + // Setup + keyring := testkeyring.New(2) + unitNetwork := network.NewUnitTestNetwork( + network.WithPreFundedAccounts(keyring.GetAllAccAddrs()...), + ) + grpcHandler := grpc.NewIntegrationHandler(unitNetwork) + txFactory := factory.New(unitNetwork, grpcHandler) + senderKey := keyring.GetKey(1) + + testCases := []struct { + name string + expectedError error + generateAccountAndArgs func() (*statedb.Account, evmtypes.EvmTxArgs) + }{ + { + name: "fail: sender is not EOA", + expectedError: errortypes.ErrInvalidType, + generateAccountAndArgs: func() (*statedb.Account, evmtypes.EvmTxArgs) { + statedbAccount := getDefaultStateDBAccount(unitNetwork, senderKey.Addr) + txArgs, err := txFactory.GenerateDefaultTxTypeArgs(senderKey.Addr, suite.ethTxType) + suite.Require().NoError(err) + + statedbAccount.CodeHash = []byte("test") + suite.Require().NoError(err) + return statedbAccount, txArgs + }, + }, + { + name: "fail: sender balance is lower than the transaction cost", + expectedError: errortypes.ErrInsufficientFunds, + generateAccountAndArgs: func() (*statedb.Account, evmtypes.EvmTxArgs) { + statedbAccount := getDefaultStateDBAccount(unitNetwork, senderKey.Addr) + txArgs, err := txFactory.GenerateDefaultTxTypeArgs(senderKey.Addr, suite.ethTxType) + suite.Require().NoError(err) + + // Make tx cost greater than balance + balanceResp, err := grpcHandler.GetBalance(senderKey.AccAddr, unitNetwork.GetDenom()) + suite.Require().NoError(err) + + invalidAmount := balanceResp.Balance.Amount.Add(math.NewInt(100)) + txArgs.Amount = invalidAmount.BigInt() + return statedbAccount, txArgs + }, + }, + { + name: "fail: tx cost is negative", + expectedError: errortypes.ErrInvalidCoins, + generateAccountAndArgs: func() (*statedb.Account, evmtypes.EvmTxArgs) { + statedbAccount := getDefaultStateDBAccount(unitNetwork, senderKey.Addr) + txArgs, err := txFactory.GenerateDefaultTxTypeArgs(senderKey.Addr, suite.ethTxType) + suite.Require().NoError(err) + + // Make tx cost negative. This has to be a big value because + // it has to be bigger than the fee for the full cost to be negative + invalidAmount := big.NewInt(-1e18) + txArgs.Amount = invalidAmount + return statedbAccount, txArgs + }, + }, + { + name: "success: tx is successful and account is created if its nil", + expectedError: errortypes.ErrInsufficientFunds, + generateAccountAndArgs: func() (*statedb.Account, evmtypes.EvmTxArgs) { + txArgs, err := txFactory.GenerateDefaultTxTypeArgs(senderKey.Addr, suite.ethTxType) + suite.Require().NoError(err) + return nil, txArgs + }, + }, + { + name: "success: tx is successful if account is EOA and exists", + expectedError: nil, + generateAccountAndArgs: func() (*statedb.Account, evmtypes.EvmTxArgs) { + statedbAccount := getDefaultStateDBAccount(unitNetwork, senderKey.Addr) + txArgs, err := txFactory.GenerateDefaultTxTypeArgs(senderKey.Addr, suite.ethTxType) + suite.Require().NoError(err) + return statedbAccount, txArgs + }, + }, + } + + for _, tc := range testCases { + suite.Run(fmt.Sprintf("%v_%v", evmtypes.GetTxTypeName(suite.ethTxType), tc.name), func() { + // Perform test logic + statedbAccount, txArgs := tc.generateAccountAndArgs() + txData, err := txArgs.ToTxData() + suite.Require().NoError(err) + + // Function to be tested + err = evm.VerifyAccountBalance( + unitNetwork.GetContext(), + unitNetwork.App.AccountKeeper, + statedbAccount, + senderKey.Addr, + txData, + ) + + if tc.expectedError != nil { + suite.Require().Error(err) + suite.Contains(err.Error(), tc.expectedError.Error()) + } else { + suite.Require().NoError(err) + } + // Make sure the account is created either wa + acc, err := grpcHandler.GetAccount(senderKey.AccAddr.String()) + suite.Require().NoError(err) + suite.Require().NotEmpty(acc) + + // Clean block for next test + err = unitNetwork.NextBlock() + suite.Require().NoError(err) + }) + } +} + +func getDefaultStateDBAccount(unitNetwork *network.UnitTestNetwork, addr common.Address) *statedb.Account { + statedb := unitNetwork.GetStateDB() + return statedb.Keeper().GetAccount(unitNetwork.GetContext(), addr) +} diff --git a/ante/evm/07_can_transfer.go b/ante/evm/07_can_transfer.go new file mode 100644 index 0000000..ee8c787 --- /dev/null +++ b/ante/evm/07_can_transfer.go @@ -0,0 +1,61 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package evm + +import ( + "math/big" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/params" + "github.com/evmos/evmos/v19/x/evm/statedb" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + anteinterfaces "github.com/evmos/os/ante/interfaces" +) + +// CanTransfer checks if the sender is allowed to transfer funds according to the EVM block +func CanTransfer( + ctx sdk.Context, + evmKeeper anteinterfaces.EVMKeeper, + msg core.Message, + baseFee *big.Int, + ethCfg *params.ChainConfig, + params evmtypes.Params, + isLondon bool, +) error { + if isLondon && msg.GasFeeCap().Cmp(baseFee) < 0 { + return errorsmod.Wrapf( + errortypes.ErrInsufficientFee, + "max fee per gas less than block base fee (%s < %s)", + msg.GasFeeCap(), baseFee, + ) + } + + // NOTE: pass in an empty coinbase address and nil tracer as we don't need them for the check below + cfg := &statedb.EVMConfig{ + ChainConfig: ethCfg, + Params: params, + CoinBase: common.Address{}, + BaseFee: baseFee, + } + + stateDB := statedb.New(ctx, evmKeeper, statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash().Bytes()))) + evm := evmKeeper.NewEVM(ctx, msg, cfg, evmtypes.NewNoOpTracer(), stateDB) + + // check that caller has enough balance to cover asset transfer for **topmost** call + // NOTE: here the gas consumed is from the context with the infinite gas meter + if msg.Value().Sign() > 0 && !evm.Context.CanTransfer(stateDB, msg.From(), msg.Value()) { + return errorsmod.Wrapf( + errortypes.ErrInsufficientFunds, + "failed to transfer %s from address %s using the EVM block context transfer function", + msg.Value(), + msg.From(), + ) + } + + return nil +} diff --git a/ante/evm/07_can_transfer_test.go b/ante/evm/07_can_transfer_test.go new file mode 100644 index 0000000..fcb445c --- /dev/null +++ b/ante/evm/07_can_transfer_test.go @@ -0,0 +1,105 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package evm_test + +import ( + "fmt" + "math/big" + + "cosmossdk.io/math" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/evmos/evmos/v19/testutil/integration/evmos/factory" + "github.com/evmos/evmos/v19/testutil/integration/evmos/grpc" + testkeyring "github.com/evmos/evmos/v19/testutil/integration/evmos/keyring" + "github.com/evmos/evmos/v19/testutil/integration/evmos/network" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + "github.com/evmos/os/ante/evm" +) + +func (suite *EvmAnteTestSuite) TestCanTransfer() { + keyring := testkeyring.New(1) + unitNetwork := network.NewUnitTestNetwork( + network.WithPreFundedAccounts(keyring.GetAllAccAddrs()...), + ) + grpcHandler := grpc.NewIntegrationHandler(unitNetwork) + txFactory := factory.New(unitNetwork, grpcHandler) + senderKey := keyring.GetKey(0) + + testCases := []struct { + name string + expectedError error + isLondon bool + malleate func(txArgs *evmtypes.EvmTxArgs) + }{ + { + name: "fail: isLondon and insufficient fee", + expectedError: errortypes.ErrInsufficientFee, + isLondon: true, + malleate: func(txArgs *evmtypes.EvmTxArgs) { + txArgs.GasFeeCap = big.NewInt(0) + }, + }, + { + name: "fail: invalid tx with insufficient balance", + expectedError: errortypes.ErrInsufficientFunds, + isLondon: true, + malleate: func(txArgs *evmtypes.EvmTxArgs) { + balanceResp, err := grpcHandler.GetBalance(senderKey.AccAddr, unitNetwork.GetDenom()) + suite.Require().NoError(err) + invalidAmount := balanceResp.Balance.Amount.Add(math.NewInt(1)).BigInt() + txArgs.Amount = invalidAmount + }, + }, + { + name: "success: valid tx and sufficient balance", + expectedError: nil, + isLondon: true, + malleate: func(*evmtypes.EvmTxArgs) { + }, + }, + } + + for _, tc := range testCases { + suite.Run(fmt.Sprintf("%v_%v", evmtypes.GetTxTypeName(suite.ethTxType), tc.name), func() { + baseFeeResp, err := grpcHandler.GetBaseFee() + suite.Require().NoError(err) + ethCfg := unitNetwork.GetEVMChainConfig() + evmParams, err := grpcHandler.GetEvmParams() + suite.Require().NoError(err) + ctx := unitNetwork.GetContext() + signer := gethtypes.MakeSigner(ethCfg, big.NewInt(ctx.BlockHeight())) + txArgs, err := txFactory.GenerateDefaultTxTypeArgs(senderKey.Addr, suite.ethTxType) + suite.Require().NoError(err) + txArgs.Amount = big.NewInt(100) + + tc.malleate(&txArgs) + + msg := evmtypes.NewTx(&txArgs) + msg.From = senderKey.Addr.String() + signMsg, err := txFactory.SignMsgEthereumTx(senderKey.Priv, *msg) + suite.Require().NoError(err) + coreMsg, err := signMsg.AsMessage(signer, baseFeeResp.BaseFee.BigInt()) + suite.Require().NoError(err) + + // Function under test + err = evm.CanTransfer( + unitNetwork.GetContext(), + unitNetwork.App.EvmKeeper, + coreMsg, + baseFeeResp.BaseFee.BigInt(), + ethCfg, + evmParams.Params, + tc.isLondon, + ) + + if tc.expectedError != nil { + suite.Require().Error(err) + suite.Contains(err.Error(), tc.expectedError.Error()) + + } else { + suite.Require().NoError(err) + } + }) + } +} diff --git a/ante/evm/08_gas_consume.go b/ante/evm/08_gas_consume.go new file mode 100644 index 0000000..28d5f94 --- /dev/null +++ b/ante/evm/08_gas_consume.go @@ -0,0 +1,150 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package evm + +import ( + "math/big" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + sdktypes "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/ethereum/go-ethereum/common" + "github.com/evmos/evmos/v19/types" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + anteinterfaces "github.com/evmos/os/ante/interfaces" +) + +// UpdateCumulativeGasWanted updates the cumulative gas wanted +func UpdateCumulativeGasWanted( + ctx sdktypes.Context, + msgGasWanted uint64, + maxTxGasWanted uint64, + cumulativeGasWanted uint64, +) uint64 { + if ctx.IsCheckTx() && maxTxGasWanted != 0 { + // We can't trust the tx gas limit, because we'll refund the unused gas. + if msgGasWanted > maxTxGasWanted { + cumulativeGasWanted += maxTxGasWanted + } else { + cumulativeGasWanted += msgGasWanted + } + } else { + cumulativeGasWanted += msgGasWanted + } + return cumulativeGasWanted +} + +type ConsumeGasKeepers struct { + Bank anteinterfaces.BankKeeper + Distribution anteinterfaces.DistributionKeeper + Evm anteinterfaces.EVMKeeper + Staking anteinterfaces.StakingKeeper +} + +// ConsumeFeesAndEmitEvent deduces fees from sender and emits the event +func ConsumeFeesAndEmitEvent( + ctx sdktypes.Context, + keepers *ConsumeGasKeepers, + fees sdktypes.Coins, + from sdktypes.AccAddress, +) error { + if err := deductFees( + ctx, + keepers, + fees, + from, + ); err != nil { + return err + } + + ctx.EventManager().EmitEvent( + sdktypes.NewEvent( + sdktypes.EventTypeTx, + sdktypes.NewAttribute(sdktypes.AttributeKeyFee, fees.String()), + ), + ) + return nil +} + +// deductFee checks if the fee payer has enough funds to pay for the fees and deducts them. +// If the spendable balance is not enough, it tries to claim enough staking rewards to cover the fees. +func deductFees( + ctx sdktypes.Context, + keepers *ConsumeGasKeepers, + fees sdktypes.Coins, + feePayer sdktypes.AccAddress, +) error { + if fees.IsZero() { + return nil + } + + if err := keepers.Evm.DeductTxCostsFromUserBalance( + ctx, + fees, + common.BytesToAddress(feePayer), + ); err != nil { + return errorsmod.Wrapf(err, "failed to deduct transaction costs from user balance") + } + return nil +} + +// GetMsgPriority returns the priority of an Eth Tx capped by the minimum priority +func GetMsgPriority( + txData evmtypes.TxData, + minPriority int64, + baseFee *big.Int, +) int64 { + priority := evmtypes.GetTxPriority(txData, baseFee) + + if priority < minPriority { + minPriority = priority + } + return minPriority +} + +// TODO: (@fedekunze) Why is this necessary? This seems to be a duplicate from the CheckGasWanted function. +func CheckBlockGasLimit(ctx sdktypes.Context, gasWanted uint64, minPriority int64) (sdktypes.Context, error) { + blockGasLimit := types.BlockGasLimit(ctx) + + // return error if the tx gas is greater than the block limit (max gas) + + // NOTE: it's important here to use the gas wanted instead of the gas consumed + // from the tx gas pool. The latter only has the value so far since the + // EthSetupContextDecorator, so it will never exceed the block gas limit. + if gasWanted > blockGasLimit { + return ctx, errorsmod.Wrapf( + errortypes.ErrOutOfGas, + "tx gas (%d) exceeds block gas limit (%d)", + gasWanted, + blockGasLimit, + ) + } + + // Set tx GasMeter with a limit of GasWanted (i.e. gas limit from the Ethereum tx). + // The gas consumed will be then reset to the gas used by the state transition + // in the EVM. + + // FIXME: use a custom gas configuration that doesn't add any additional gas and only + // takes into account the gas consumed at the end of the EVM transaction. + ctx = ctx. + WithGasMeter(types.NewInfiniteGasMeterWithLimit(gasWanted)). + WithPriority(minPriority) + + return ctx, nil +} + +// UpdateCumulativeTxFee updates the cumulative transaction fee +func UpdateCumulativeTxFee( + cumulativeTxFee sdktypes.Coins, + msgFee *big.Int, + denom string, +) sdktypes.Coins { + return cumulativeTxFee.Add( + sdktypes.Coin{ + Denom: denom, + Amount: sdkmath.NewIntFromBigInt(msgFee), + }, + ) +} diff --git a/ante/evm/09_gas_consume_test.go b/ante/evm/09_gas_consume_test.go new file mode 100644 index 0000000..00d4fb8 --- /dev/null +++ b/ante/evm/09_gas_consume_test.go @@ -0,0 +1,194 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package evm_test + +import ( + sdktypes "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/evmos/evmos/v19/testutil/integration/evmos/grpc" + testkeyring "github.com/evmos/evmos/v19/testutil/integration/evmos/keyring" + "github.com/evmos/evmos/v19/testutil/integration/evmos/network" + evmante "github.com/evmos/os/ante/evm" +) + +func (suite *EvmAnteTestSuite) TestUpdateCumulativeGasWanted() { + keyring := testkeyring.New(1) + unitNetwork := network.NewUnitTestNetwork( + network.WithPreFundedAccounts(keyring.GetAllAccAddrs()...), + ) + + testCases := []struct { + name string + msgGasWanted uint64 + maxTxGasWanted uint64 + cumulativeGasWanted uint64 + getCtx func() sdktypes.Context + expectedResponse uint64 + }{ + { + name: "when is NOT checkTx and cumulativeGasWanted is 0, returns msgGasWanted", + msgGasWanted: 100, + maxTxGasWanted: 150, + cumulativeGasWanted: 0, + getCtx: func() sdktypes.Context { + return unitNetwork.GetContext().WithIsCheckTx(false) + }, + expectedResponse: 100, + }, + { + name: "when is NOT checkTx and cumulativeGasWanted has value, returns cumulativeGasWanted + msgGasWanted", + msgGasWanted: 100, + maxTxGasWanted: 150, + cumulativeGasWanted: 50, + getCtx: func() sdktypes.Context { + return unitNetwork.GetContext().WithIsCheckTx(false) + }, + expectedResponse: 150, + }, + { + name: "when is checkTx, maxTxGasWanted is not 0 and msgGasWanted > maxTxGasWanted, returns cumulativeGasWanted + maxTxGasWanted", + msgGasWanted: 200, + maxTxGasWanted: 100, + cumulativeGasWanted: 50, + getCtx: func() sdktypes.Context { + return unitNetwork.GetContext().WithIsCheckTx(true) + }, + expectedResponse: 150, + }, + { + name: "when is checkTx, maxTxGasWanted is not 0 and msgGasWanted < maxTxGasWanted, returns cumulativeGasWanted + msgGasWanted", + msgGasWanted: 50, + maxTxGasWanted: 100, + cumulativeGasWanted: 50, + getCtx: func() sdktypes.Context { + return unitNetwork.GetContext().WithIsCheckTx(true) + }, + expectedResponse: 100, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + // Function under test + gasWanted := evmante.UpdateCumulativeGasWanted( + tc.getCtx(), + tc.msgGasWanted, + tc.maxTxGasWanted, + tc.cumulativeGasWanted, + ) + + suite.Require().Equal(tc.expectedResponse, gasWanted) + }) + } +} + +// NOTE: claim rewards are not tested since there is an independent suite to test just that +func (suite *EvmAnteTestSuite) TestConsumeGasAndEmitEvent() { + keyring := testkeyring.New(1) + unitNetwork := network.NewUnitTestNetwork( + network.WithPreFundedAccounts(keyring.GetAllAccAddrs()...), + ) + grpcHandler := grpc.NewIntegrationHandler(unitNetwork) + + testCases := []struct { + name string + expectedError error + fees sdktypes.Coins + getSender func() sdktypes.AccAddress + }{ + { + name: "success: fees are zero and event emitted", + expectedError: nil, + fees: sdktypes.Coins{}, + getSender: func() sdktypes.AccAddress { + // Return prefunded sender + return keyring.GetKey(0).AccAddr + }, + }, + { + name: "success: there are non zero fees, user has sufficient bank balances and event emitted", + expectedError: nil, + fees: sdktypes.Coins{ + sdktypes.NewCoin(unitNetwork.GetDenom(), sdktypes.NewInt(1000)), + }, + getSender: func() sdktypes.AccAddress { + // Return prefunded sender + return keyring.GetKey(0).AccAddr + }, + }, + { + name: "fail: insufficient user balance, event is NOT emitted", + expectedError: sdkerrors.ErrInsufficientFee, + fees: sdktypes.Coins{ + sdktypes.NewCoin(unitNetwork.GetDenom(), sdktypes.NewInt(1000)), + }, + getSender: func() sdktypes.AccAddress { + // Return unfunded account + index := keyring.AddKey() + return keyring.GetKey(index).AccAddr + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + keepers := &evmante.ConsumeGasKeepers{ + Bank: unitNetwork.App.BankKeeper, + Distribution: unitNetwork.App.DistrKeeper, + Evm: unitNetwork.App.EvmKeeper, + Staking: unitNetwork.App.StakingKeeper, + } + sender := tc.getSender() + prevBalance, err := grpcHandler.GetAllBalances( + sender, + ) + suite.Require().NoError(err) + + // Function under test + err = evmante.ConsumeFeesAndEmitEvent( + unitNetwork.GetContext(), + keepers, + tc.fees, + sender, + ) + + if tc.expectedError != nil { + suite.Require().Error(err) + suite.Contains(err.Error(), tc.expectedError.Error()) + + // Check events are not present + events := unitNetwork.GetContext().EventManager().Events() + suite.Require().Zero(len(events)) + } else { + suite.Require().NoError(err) + + // Check fees are deducted + afterBalance, err := grpcHandler.GetAllBalances( + sender, + ) + suite.Require().NoError(err) + expectedBalance := prevBalance.Balances.Sub(tc.fees...) + suite.Require().True( + expectedBalance.IsEqual(afterBalance.Balances), + ) + + // Event to be emitted + expectedEvent := sdktypes.NewEvent( + sdktypes.EventTypeTx, + sdktypes.NewAttribute(sdktypes.AttributeKeyFee, tc.fees.String()), + ) + // Check events are present + events := unitNetwork.GetContext().EventManager().Events() + suite.Require().NotZero(len(events)) + suite.Require().Contains( + events, + expectedEvent, + ) + } + + // Reset the context + err = unitNetwork.NextBlock() + suite.Require().NoError(err) + }) + } +} diff --git a/ante/evm/09_increment_sequence.go b/ante/evm/09_increment_sequence.go new file mode 100644 index 0000000..70d9f67 --- /dev/null +++ b/ante/evm/09_increment_sequence.go @@ -0,0 +1,41 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package evm + +import ( + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + evmtypes "github.com/evmos/evmos/v19/x/evm/types" +) + +// IncrementNonce increments the sequence of the account. +func IncrementNonce( + ctx sdk.Context, + accountKeeper evmtypes.AccountKeeper, + account authtypes.AccountI, + txNonce uint64, +) error { + nonce := account.GetSequence() + // we merged the nonce verification to nonce increment, so when tx includes multiple messages + // with same sender, they'll be accepted. + if txNonce != nonce { + return errorsmod.Wrapf( + errortypes.ErrInvalidSequence, + "invalid nonce; got %d, expected %d", txNonce, nonce, + ) + } + + nonce++ + + if err := account.SetSequence(nonce); err != nil { + return errorsmod.Wrapf(err, "failed to set sequence to %d", nonce) + } + + accountKeeper.SetAccount(ctx, account) + return nil +} diff --git a/ante/evm/10_gas_wanted.go b/ante/evm/10_gas_wanted.go new file mode 100644 index 0000000..7710ad2 --- /dev/null +++ b/ante/evm/10_gas_wanted.go @@ -0,0 +1,84 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package evm + +import ( + "math/big" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + anteinterfaces "github.com/evmos/os/ante/interfaces" + "github.com/evmos/os/types" +) + +// GasWantedDecorator keeps track of the gasWanted amount on the current block in transient store +// for BaseFee calculation. +// NOTE: This decorator does not perform any validation +type GasWantedDecorator struct { + evmKeeper anteinterfaces.EVMKeeper + feeMarketKeeper anteinterfaces.FeeMarketKeeper +} + +// NewGasWantedDecorator creates a new NewGasWantedDecorator +func NewGasWantedDecorator( + evmKeeper anteinterfaces.EVMKeeper, + feeMarketKeeper anteinterfaces.FeeMarketKeeper, +) GasWantedDecorator { + return GasWantedDecorator{ + evmKeeper, + feeMarketKeeper, + } +} + +func (gwd GasWantedDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + evmParams := gwd.evmKeeper.GetParams(ctx) + chainCfg := evmParams.GetChainConfig() + ethCfg := chainCfg.EthereumConfig(gwd.evmKeeper.ChainID()) + + blockHeight := big.NewInt(ctx.BlockHeight()) + isLondon := ethCfg.IsLondon(blockHeight) + + if err := CheckGasWanted(ctx, gwd.feeMarketKeeper, tx, isLondon); err != nil { + return ctx, err + } + + return next(ctx, tx, simulate) +} + +func CheckGasWanted(ctx sdk.Context, feeMarketKeeper anteinterfaces.FeeMarketKeeper, tx sdk.Tx, isLondon bool) error { + if !isLondon { + return nil + } + + feeTx, ok := tx.(sdk.FeeTx) + if !ok { + return nil + } + + gasWanted := feeTx.GetGas() + + // return error if the tx gas is greater than the block limit (max gas) + blockGasLimit := types.BlockGasLimit(ctx) + if gasWanted > blockGasLimit { + return errorsmod.Wrapf( + errortypes.ErrOutOfGas, + "tx gas (%d) exceeds block gas limit (%d)", + gasWanted, + blockGasLimit, + ) + } + + isBaseFeeEnabled := feeMarketKeeper.GetBaseFeeEnabled(ctx) + if !isBaseFeeEnabled { + return nil + } + + // Add total gasWanted to cumulative in block transientStore in FeeMarket module + if _, err := feeMarketKeeper.AddTransientGasWanted(ctx, gasWanted); err != nil { + return errorsmod.Wrapf(err, "failed to add gas wanted to transient store") + } + + return nil +} diff --git a/ante/evm/10_incremenet_sequence_test.go b/ante/evm/10_incremenet_sequence_test.go new file mode 100644 index 0000000..bd59db1 --- /dev/null +++ b/ante/evm/10_incremenet_sequence_test.go @@ -0,0 +1,72 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package evm_test + +import ( + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/evmos/evmos/v19/testutil/integration/evmos/grpc" + testkeyring "github.com/evmos/evmos/v19/testutil/integration/evmos/keyring" + "github.com/evmos/evmos/v19/testutil/integration/evmos/network" + "github.com/evmos/os/ante/evm" +) + +func (suite *EvmAnteTestSuite) TestIncrementSequence() { + keyring := testkeyring.New(1) + unitNetwork := network.NewUnitTestNetwork( + network.WithPreFundedAccounts(keyring.GetAllAccAddrs()...), + ) + grpcHandler := grpc.NewIntegrationHandler(unitNetwork) + accAddr := keyring.GetAccAddr(0) + + testCases := []struct { + name string + expectedError error + malleate func(acct authtypes.AccountI) uint64 + }{ + { + name: "fail: invalid sequence", + expectedError: errortypes.ErrInvalidSequence, + malleate: func(acct authtypes.AccountI) uint64 { + return acct.GetSequence() + 1 + }, + }, + { + name: "success: increments sequence", + expectedError: nil, + malleate: func(acct authtypes.AccountI) uint64 { + return acct.GetSequence() + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + account, err := grpcHandler.GetAccount(accAddr.String()) + suite.Require().NoError(err) + preSequence := account.GetSequence() + + nonce := tc.malleate(account) + + // Function under test + err = evm.IncrementNonce( + unitNetwork.GetContext(), + unitNetwork.App.AccountKeeper, + account, + nonce, + ) + + if tc.expectedError != nil { + suite.Require().Error(err) + suite.Contains(err.Error(), tc.expectedError.Error()) + } else { + suite.Require().NoError(err) + + suite.Require().Equal(preSequence+1, account.GetSequence()) + updatedAccount, err := grpcHandler.GetAccount(accAddr.String()) + suite.Require().NoError(err) + suite.Require().Equal(preSequence+1, updatedAccount.GetSequence()) + } + }) + } +} diff --git a/ante/evm/11_emit_event.go b/ante/evm/11_emit_event.go new file mode 100644 index 0000000..9a9c4b5 --- /dev/null +++ b/ante/evm/11_emit_event.go @@ -0,0 +1,60 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package evm + +import ( + "strconv" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + anteinterfaces "github.com/evmos/os/ante/interfaces" +) + +// EthEmitEventDecorator emit events in ante handler in case of tx execution failed (out of block gas limit). +type EthEmitEventDecorator struct { + evmKeeper anteinterfaces.EVMKeeper +} + +// NewEthEmitEventDecorator creates a new EthEmitEventDecorator +func NewEthEmitEventDecorator(evmKeeper anteinterfaces.EVMKeeper) EthEmitEventDecorator { + return EthEmitEventDecorator{evmKeeper} +} + +// AnteHandle emits some basic events for the eth messages +func (eeed EthEmitEventDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + // After eth tx passed ante handler, the fee is deducted and nonce increased, it shouldn't be ignored by json-rpc, + // we need to emit some basic events at the very end of ante handler to be indexed by tendermint. + blockTxIndex := eeed.evmKeeper.GetTxIndexTransient(ctx) + + for i, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evmtypes.MsgEthereumTx)(nil)) + } + + txIdx := uint64(i) // nosec: G701 + EmitTxHashEvent(ctx, msgEthTx, blockTxIndex, txIdx) + } + + return next(ctx, tx, simulate) +} + +// EmitTxHashEvent emits the Ethereum tx +// +// FIXME: This is Technical debt. Ideally the sdk.Tx hash should be the Ethereum +// tx hash (msg.Hash) instead of using events for indexing Eth txs. +// TxIndex should be included in the header vote extension as part of ABCI++ +func EmitTxHashEvent(ctx sdk.Context, msg *evmtypes.MsgEthereumTx, blockTxIndex, msgIndex uint64) { + // emit ethereum tx hash as an event so that it can be indexed by Tendermint for query purposes + // it's emitted in ante handler, so we can query failed transaction (out of block gas limit). + ctx.EventManager().EmitEvent( + sdk.NewEvent( + evmtypes.EventTypeEthereumTx, + sdk.NewAttribute(evmtypes.AttributeKeyEthereumTxHash, msg.Hash), + sdk.NewAttribute(evmtypes.AttributeKeyTxIndex, strconv.FormatUint(blockTxIndex+msgIndex, 10)), // #nosec G701 + ), + ) +} diff --git a/ante/evm/11_gas_wanted_test.go b/ante/evm/11_gas_wanted_test.go new file mode 100644 index 0000000..7cabcda --- /dev/null +++ b/ante/evm/11_gas_wanted_test.go @@ -0,0 +1,120 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package evm_test + +import ( + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + + sdktypes "github.com/cosmos/cosmos-sdk/types" + "github.com/evmos/evmos/v19/testutil/integration/evmos/factory" + "github.com/evmos/evmos/v19/testutil/integration/evmos/grpc" + testkeyring "github.com/evmos/evmos/v19/testutil/integration/evmos/keyring" + "github.com/evmos/evmos/v19/testutil/integration/evmos/network" + "github.com/evmos/os/ante/evm" +) + +func (suite *EvmAnteTestSuite) TestCheckGasWanted() { + keyring := testkeyring.New(1) + unitNetwork := network.NewUnitTestNetwork( + network.WithPreFundedAccounts(keyring.GetAllAccAddrs()...), + ) + grpcHandler := grpc.NewIntegrationHandler(unitNetwork) + txFactory := factory.New(unitNetwork, grpcHandler) + commonGasLimit := uint64(100000) + + testCases := []struct { + name string + expectedError error + getCtx func() sdktypes.Context + isLondon bool + expectedTransientGasWanted uint64 + }{ + { + name: "success: if isLondon false it should not error", + expectedError: nil, + getCtx: func() sdktypes.Context { + // Even if the gasWanted is more than the blockGasLimit, it should not error + blockMeter := sdktypes.NewGasMeter(commonGasLimit - 10000) + return unitNetwork.GetContext().WithBlockGasMeter(blockMeter) + }, + isLondon: false, + expectedTransientGasWanted: 0, + }, + { + name: "success: gasWanted is less than blockGasLimit", + expectedError: nil, + getCtx: func() sdktypes.Context { + blockMeter := sdktypes.NewGasMeter(commonGasLimit + 10000) + return unitNetwork.GetContext().WithBlockGasMeter(blockMeter) + }, + isLondon: true, + expectedTransientGasWanted: commonGasLimit, + }, + { + name: "fail: gasWanted is more than blockGasLimit", + expectedError: errortypes.ErrOutOfGas, + getCtx: func() sdktypes.Context { + blockMeter := sdktypes.NewGasMeter(commonGasLimit - 10000) + return unitNetwork.GetContext().WithBlockGasMeter(blockMeter) + }, + isLondon: true, + expectedTransientGasWanted: 0, + }, + { + name: "success: gasWanted is less than blockGasLimit and basefee param is disabled", + expectedError: nil, + getCtx: func() sdktypes.Context { + // Set basefee param to false + feeMarketParams, err := grpcHandler.GetFeeMarketParams() + suite.Require().NoError(err) + feeMarketParams.Params.NoBaseFee = true + err = unitNetwork.UpdateFeeMarketParams(feeMarketParams.Params) + suite.Require().NoError(err) + + blockMeter := sdktypes.NewGasMeter(commonGasLimit + 10000) + return unitNetwork.GetContext().WithBlockGasMeter(blockMeter) + }, + isLondon: true, + expectedTransientGasWanted: 0, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + sender := keyring.GetKey(0) + txArgs, err := txFactory.GenerateDefaultTxTypeArgs( + sender.Addr, + suite.ethTxType, + ) + suite.Require().NoError(err) + txArgs.GasLimit = commonGasLimit + tx, err := txFactory.GenerateSignedEthTx(sender.Priv, txArgs) + suite.Require().NoError(err) + + ctx := tc.getCtx() + + // Function under test + err = evm.CheckGasWanted( + ctx, + unitNetwork.App.FeeMarketKeeper, + tx, + tc.isLondon, + ) + + if tc.expectedError != nil { + suite.Require().Error(err) + suite.Contains(err.Error(), tc.expectedError.Error()) + } else { + suite.Require().NoError(err) + transientGasWanted := unitNetwork.App.FeeMarketKeeper.GetTransientGasWanted( + unitNetwork.GetContext(), + ) + suite.Require().Equal(tc.expectedTransientGasWanted, transientGasWanted) + } + + // Start from a fresh block and ctx + err = unitNetwork.NextBlock() + suite.Require().NoError(err) + }) + } +} diff --git a/ante/evm/fee_checker.go b/ante/evm/fee_checker.go new file mode 100644 index 0000000..4d8416b --- /dev/null +++ b/ante/evm/fee_checker.go @@ -0,0 +1,161 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package evm + +import ( + "math" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + "github.com/ethereum/go-ethereum/params" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + anteinterfaces "github.com/evmos/os/ante/interfaces" + evmostypes "github.com/evmos/os/types" +) + +// NewDynamicFeeChecker returns a `TxFeeChecker` that applies a dynamic fee to +// Cosmos txs using the EIP-1559 fee market logic. +// This can be called in both CheckTx and deliverTx modes. +// a) feeCap = tx.fees / tx.gas +// b) tipFeeCap = tx.MaxPriorityPrice (default) or MaxInt64 +// - when `ExtensionOptionDynamicFeeTx` is omitted, `tipFeeCap` defaults to `MaxInt64`. +// - when london hardfork is not enabled, it falls back to SDK default behavior (validator min-gas-prices). +// - Tx priority is set to `effectiveGasPrice / DefaultPriorityReduction`. +func NewDynamicFeeChecker(k anteinterfaces.DynamicFeeEVMKeeper) authante.TxFeeChecker { + return func(ctx sdk.Context, tx sdk.Tx) (sdk.Coins, int64, error) { + feeTx, ok := tx.(sdk.FeeTx) + if !ok { + return sdk.Coins{}, 0, errorsmod.Wrap(errortypes.ErrTxDecode, "Tx must be a FeeTx") + } + // TODO: in the e2e test, if the fee in the genesis transaction meet the baseFee and minGasPrice in the feemarket, we can remove this code + if ctx.BlockHeight() == 0 { + // genesis transactions: fallback to min-gas-price logic + return checkTxFeeWithValidatorMinGasPrices(ctx, feeTx) + } + params := k.GetParams(ctx) + denom := params.EvmDenom + ethCfg := params.ChainConfig.EthereumConfig(k.ChainID()) + + return FeeChecker(ctx, k, denom, ethCfg, feeTx) + } +} + +// FeeChecker returns the effective fee and priority for a given transaction. +func FeeChecker( + ctx sdk.Context, + k anteinterfaces.DynamicFeeEVMKeeper, + denom string, + ethConfig *params.ChainConfig, + feeTx sdk.FeeTx, +) (sdk.Coins, int64, error) { + baseFee := k.GetBaseFee(ctx, ethConfig) + if baseFee == nil { + // london hardfork is not enabled: fallback to min-gas-prices logic + return checkTxFeeWithValidatorMinGasPrices(ctx, feeTx) + } + + // default to `MaxInt64` when there's no extension option. + maxPriorityPrice := sdkmath.NewInt(math.MaxInt64) + + // get the priority tip cap from the extension option. + if hasExtOptsTx, ok := feeTx.(authante.HasExtensionOptionsTx); ok { + for _, opt := range hasExtOptsTx.GetExtensionOptions() { + if extOpt, ok := opt.GetCachedValue().(*evmostypes.ExtensionOptionDynamicFeeTx); ok { + maxPriorityPrice = extOpt.MaxPriorityPrice + break + } + } + } + + // priority fee cannot be negative + if maxPriorityPrice.IsNegative() { + return nil, 0, errorsmod.Wrapf(errortypes.ErrInsufficientFee, "max priority price cannot be negative") + } + + gas := feeTx.GetGas() + feeCoins := feeTx.GetFee() + fee := feeCoins.AmountOfNoDenomValidation(denom) + + feeCap := fee.Quo(sdkmath.NewIntFromUint64(gas)) + baseFeeInt := sdkmath.NewIntFromBigInt(baseFee) + + if feeCap.LT(baseFeeInt) { + return nil, 0, errorsmod.Wrapf(errortypes.ErrInsufficientFee, "gas prices too low, got: %s%s required: %s%s. Please retry using a higher gas price or a higher fee", feeCap, denom, baseFeeInt, denom) + } + + // calculate the effective gas price using the EIP-1559 logic. + effectivePrice := sdkmath.NewIntFromBigInt(evmtypes.EffectiveGasPrice(baseFeeInt.BigInt(), feeCap.BigInt(), maxPriorityPrice.BigInt())) + + // NOTE: create a new coins slice without having to validate the denom + effectiveFee := sdk.Coins{ + { + Denom: denom, + Amount: effectivePrice.Mul(sdkmath.NewIntFromUint64(gas)), + }, + } + + bigPriority := effectivePrice.Sub(baseFeeInt).Quo(evmtypes.DefaultPriorityReduction) + priority := int64(math.MaxInt64) + + if bigPriority.IsInt64() { + priority = bigPriority.Int64() + } + + return effectiveFee, priority, nil +} + +// checkTxFeeWithValidatorMinGasPrices implements the default fee logic, where the minimum price per +// unit of gas is fixed and set by each validator, and the tx priority is computed from the gas price. +func checkTxFeeWithValidatorMinGasPrices(ctx sdk.Context, tx sdk.FeeTx) (sdk.Coins, int64, error) { + feeCoins := tx.GetFee() + minGasPrices := ctx.MinGasPrices() + gas := int64(tx.GetGas()) //#nosec G701 -- checked for int overflow on ValidateBasic() + + // Ensure that the provided fees meet a minimum threshold for the validator, + // if this is a CheckTx. This is only for local mempool purposes, and thus + // is only ran on check tx. + if ctx.IsCheckTx() && !minGasPrices.IsZero() { + requiredFees := make(sdk.Coins, len(minGasPrices)) + + // Determine the required fees by multiplying each required minimum gas + // price by the gas limit, where fee = ceil(minGasPrice * gasLimit). + glDec := sdkmath.LegacyNewDec(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, 0, errorsmod.Wrapf(errortypes.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins, requiredFees) + } + } + + priority := getTxPriority(feeCoins, gas) + return feeCoins, priority, nil +} + +// getTxPriority returns a naive tx priority based on the amount of the smallest denomination of the gas price +// provided in a transaction. +func getTxPriority(fees sdk.Coins, gas int64) int64 { + var priority int64 + + for _, fee := range fees { + gasPrice := fee.Amount.QuoRaw(gas) + amt := gasPrice.Quo(evmtypes.DefaultPriorityReduction) + p := int64(math.MaxInt64) + + if amt.IsInt64() { + p = amt.Int64() + } + + if priority == 0 || p < priority { + priority = p + } + } + + return priority +} diff --git a/ante/evm/fee_checker_test.go b/ante/evm/fee_checker_test.go new file mode 100644 index 0000000..cde359c --- /dev/null +++ b/ante/evm/fee_checker_test.go @@ -0,0 +1,243 @@ +package evm + +import ( + "math/big" + "testing" + + "cosmossdk.io/math" + "github.com/stretchr/testify/require" + + "github.com/cometbft/cometbft/libs/log" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + "github.com/ethereum/go-ethereum/params" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + anteinterfaces "github.com/evmos/os/ante/interfaces" + "github.com/evmos/os/encoding" + "github.com/evmos/os/types" +) + +var _ anteinterfaces.DynamicFeeEVMKeeper = MockEVMKeeper{} + +type MockEVMKeeper struct { + BaseFee *big.Int + EnableLondonHF bool +} + +func (m MockEVMKeeper) GetBaseFee(_ sdk.Context, _ *params.ChainConfig) *big.Int { + if m.EnableLondonHF { + return m.BaseFee + } + return nil +} + +func (m MockEVMKeeper) GetParams(_ sdk.Context) evmtypes.Params { + return evmtypes.DefaultParams() +} + +func (m MockEVMKeeper) ChainID() *big.Int { + return big.NewInt(9000) +} + +func TestSDKTxFeeChecker(t *testing.T) { + // testCases: + // fallback + // genesis tx + // checkTx, validate with min-gas-prices + // deliverTx, no validation + // dynamic fee + // with extension option + // without extension option + // london hardfork enableness + encodingConfig := encoding.MakeConfig(module.NewBasicManager()) + minGasPrices := sdk.NewDecCoins(sdk.NewDecCoin(evmtypes.DefaultEVMDenom, math.NewInt(10))) + + genesisCtx := sdk.NewContext(nil, tmproto.Header{}, false, log.NewNopLogger()) + checkTxCtx := sdk.NewContext(nil, tmproto.Header{Height: 1}, true, log.NewNopLogger()).WithMinGasPrices(minGasPrices) + deliverTxCtx := sdk.NewContext(nil, tmproto.Header{Height: 1}, false, log.NewNopLogger()) + + testCases := []struct { + name string + ctx sdk.Context + keeper anteinterfaces.DynamicFeeEVMKeeper + buildTx func() sdk.FeeTx + expFees string + expPriority int64 + expSuccess bool + }{ + { + "success, genesis tx", + genesisCtx, + MockEVMKeeper{}, + func() sdk.FeeTx { + return encodingConfig.TxConfig.NewTxBuilder().GetTx() + }, + "", + 0, + true, + }, + { + "fail, min-gas-prices", + checkTxCtx, + MockEVMKeeper{}, + func() sdk.FeeTx { + return encodingConfig.TxConfig.NewTxBuilder().GetTx() + }, + "", + 0, + false, + }, + { + "success, min-gas-prices", + checkTxCtx, + MockEVMKeeper{}, + func() sdk.FeeTx { + txBuilder := encodingConfig.TxConfig.NewTxBuilder() + txBuilder.SetGasLimit(1) + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(10)))) + return txBuilder.GetTx() + }, + "10aevmos", + 0, + true, + }, + { + "success, min-gas-prices deliverTx", + deliverTxCtx, + MockEVMKeeper{}, + func() sdk.FeeTx { + return encodingConfig.TxConfig.NewTxBuilder().GetTx() + }, + "", + 0, + true, + }, + { + "fail, dynamic fee", + deliverTxCtx, + MockEVMKeeper{ + EnableLondonHF: true, BaseFee: big.NewInt(1), + }, + func() sdk.FeeTx { + txBuilder := encodingConfig.TxConfig.NewTxBuilder() + txBuilder.SetGasLimit(1) + return txBuilder.GetTx() + }, + "", + 0, + false, + }, + { + "success, dynamic fee", + deliverTxCtx, + MockEVMKeeper{ + EnableLondonHF: true, BaseFee: big.NewInt(10), + }, + func() sdk.FeeTx { + txBuilder := encodingConfig.TxConfig.NewTxBuilder() + txBuilder.SetGasLimit(1) + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(10)))) + return txBuilder.GetTx() + }, + "10aevmos", + 0, + true, + }, + { + "success, dynamic fee priority", + deliverTxCtx, + MockEVMKeeper{ + EnableLondonHF: true, BaseFee: big.NewInt(10), + }, + func() sdk.FeeTx { + txBuilder := encodingConfig.TxConfig.NewTxBuilder() + txBuilder.SetGasLimit(1) + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(10).Mul(evmtypes.DefaultPriorityReduction).Add(math.NewInt(10))))) + return txBuilder.GetTx() + }, + "10000010aevmos", + 10, + true, + }, + { + "success, dynamic fee empty tipFeeCap", + deliverTxCtx, + MockEVMKeeper{ + EnableLondonHF: true, BaseFee: big.NewInt(10), + }, + func() sdk.FeeTx { + txBuilder := encodingConfig.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder) + txBuilder.SetGasLimit(1) + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(10).Mul(evmtypes.DefaultPriorityReduction)))) + + option, err := codectypes.NewAnyWithValue(&types.ExtensionOptionDynamicFeeTx{}) + require.NoError(t, err) + txBuilder.SetExtensionOptions(option) + return txBuilder.GetTx() + }, + "10aevmos", + 0, + true, + }, + { + "success, dynamic fee tipFeeCap", + deliverTxCtx, + MockEVMKeeper{ + EnableLondonHF: true, BaseFee: big.NewInt(10), + }, + func() sdk.FeeTx { + txBuilder := encodingConfig.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder) + txBuilder.SetGasLimit(1) + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(10).Mul(evmtypes.DefaultPriorityReduction).Add(math.NewInt(10))))) + + option, err := codectypes.NewAnyWithValue(&types.ExtensionOptionDynamicFeeTx{ + MaxPriorityPrice: math.NewInt(5).Mul(evmtypes.DefaultPriorityReduction), + }) + require.NoError(t, err) + txBuilder.SetExtensionOptions(option) + return txBuilder.GetTx() + }, + "5000010aevmos", + 5, + true, + }, + { + "fail, negative dynamic fee tipFeeCap", + deliverTxCtx, + MockEVMKeeper{ + EnableLondonHF: true, BaseFee: big.NewInt(10), + }, + func() sdk.FeeTx { + txBuilder := encodingConfig.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder) + txBuilder.SetGasLimit(1) + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(10).Mul(evmtypes.DefaultPriorityReduction).Add(math.NewInt(10))))) + + // set negative priority fee + option, err := codectypes.NewAnyWithValue(&types.ExtensionOptionDynamicFeeTx{ + MaxPriorityPrice: math.NewInt(-5).Mul(evmtypes.DefaultPriorityReduction), + }) + require.NoError(t, err) + txBuilder.SetExtensionOptions(option) + return txBuilder.GetTx() + }, + "", + 0, + false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + fees, priority, err := NewDynamicFeeChecker(tc.keeper)(tc.ctx, tc.buildTx()) + if tc.expSuccess { + require.Equal(t, tc.expFees, fees.String()) + require.Equal(t, tc.expPriority, priority) + } else { + require.Error(t, err) + } + }) + } +} diff --git a/ante/evm/setup_test.go b/ante/evm/setup_test.go new file mode 100644 index 0000000..6ce26d0 --- /dev/null +++ b/ante/evm/setup_test.go @@ -0,0 +1,132 @@ +package evm_test + +import ( + "math" + "testing" + "time" + + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/stretchr/testify/suite" + + sdkmath "cosmossdk.io/math" + "cosmossdk.io/simapp" + "github.com/cosmos/cosmos-sdk/client" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/ethereum/go-ethereum/core/types" + ante "github.com/evmos/evmos/v19/app/ante" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + feemarkettypes "github.com/evmos/evmos/v19/x/feemarket/types" + "github.com/evmos/os/encoding" + "github.com/evmos/os/ethereum/eip712" + app "github.com/evmos/os/example_chain" + "github.com/evmos/os/testutil" +) + +type AnteTestSuite struct { + suite.Suite + + ctx sdk.Context + app *app.ExampleChain + clientCtx client.Context + anteHandler sdk.AnteHandler + ethSigner types.Signer + enableFeemarket bool + enableLondonHF bool + evmParamsOption func(*evmtypes.Params) + useLegacyEIP712TypedData bool +} + +const TestGasLimit uint64 = 100000 + +func (suite *AnteTestSuite) SetupTest() { + checkTx := false + + suite.app = app.EthSetup(checkTx, func(app *app.ExampleChain, genesis simapp.GenesisState) simapp.GenesisState { + if suite.enableFeemarket { + // setup feemarketGenesis params + feemarketGenesis := feemarkettypes.DefaultGenesisState() + feemarketGenesis.Params.EnableHeight = 1 + feemarketGenesis.Params.NoBaseFee = false + // Verify feeMarket genesis + err := feemarketGenesis.Validate() + suite.Require().NoError(err) + genesis[feemarkettypes.ModuleName] = app.AppCodec().MustMarshalJSON(feemarketGenesis) + } + evmGenesis := evmtypes.DefaultGenesisState() + evmGenesis.Params.AllowUnprotectedTxs = false + if !suite.enableLondonHF { + maxInt := sdkmath.NewInt(math.MaxInt64) + evmGenesis.Params.ChainConfig.LondonBlock = &maxInt + evmGenesis.Params.ChainConfig.ArrowGlacierBlock = &maxInt + evmGenesis.Params.ChainConfig.GrayGlacierBlock = &maxInt + evmGenesis.Params.ChainConfig.MergeNetsplitBlock = &maxInt + evmGenesis.Params.ChainConfig.ShanghaiBlock = &maxInt + evmGenesis.Params.ChainConfig.CancunBlock = &maxInt + } + if suite.evmParamsOption != nil { + suite.evmParamsOption(&evmGenesis.Params) + } + genesis[evmtypes.ModuleName] = app.AppCodec().MustMarshalJSON(evmGenesis) + return genesis + }) + + suite.ctx = suite.app.BaseApp.NewContext(checkTx, tmproto.Header{Height: 2, ChainID: testutil.ExampleChainID, Time: time.Now().UTC()}) + suite.ctx = suite.ctx.WithMinGasPrices(sdk.NewDecCoins(sdk.NewDecCoin(evmtypes.DefaultEVMDenom, sdkmath.OneInt()))) + suite.ctx = suite.ctx.WithBlockGasMeter(storetypes.NewGasMeter(1000000000000000000)) + + // set staking denomination to Evmos denom + params := suite.app.StakingKeeper.GetParams(suite.ctx) + params.BondDenom = testutil.ExampleAttoDenom + err := suite.app.StakingKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + + infCtx := suite.ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + err = suite.app.AccountKeeper.SetParams(infCtx, authtypes.DefaultParams()) + suite.Require().NoError(err) + + encodingConfig := encoding.MakeConfig(app.ModuleBasics) + // We're using TestMsg amino encoding in some tests, so register it here. + encodingConfig.Amino.RegisterConcrete(&testdata.TestMsg{}, "testdata.TestMsg", nil) + eip712.SetEncodingConfig(encodingConfig) + + suite.clientCtx = client.Context{}.WithTxConfig(encodingConfig.TxConfig) + + suite.Require().NotNil(suite.app.AppCodec()) + + anteHandler := ante.NewAnteHandler(ante.HandlerOptions{ + Cdc: suite.app.AppCodec(), + AccountKeeper: suite.app.AccountKeeper, + BankKeeper: suite.app.BankKeeper, + DistributionKeeper: suite.app.DistrKeeper, + EvmKeeper: suite.app.EVMKeeper, + FeegrantKeeper: suite.app.FeeGrantKeeper, + StakingKeeper: suite.app.StakingKeeper, + FeeMarketKeeper: suite.app.FeeMarketKeeper, + SignModeHandler: encodingConfig.TxConfig.SignModeHandler(), + SigGasConsumer: ante.SigVerificationGasConsumer, + }) + + suite.anteHandler = anteHandler + suite.ethSigner = types.LatestSignerForChainID(suite.app.EVMKeeper.ChainID()) +} + +func TestAnteTestSuite(t *testing.T) { + suite.Run(t, &AnteTestSuite{ + enableLondonHF: true, + }) + + // Re-run the tests with EIP-712 Legacy encodings to ensure backwards compatibility. + // LegacyEIP712Extension should not be run with current TypedData encodings, since they are not compatible. + suite.Run(t, &AnteTestSuite{ + enableLondonHF: true, + useLegacyEIP712TypedData: true, + }) + + suite.Run(t, &AnteTestSuite{ + enableLondonHF: true, + useLegacyEIP712TypedData: true, + }) +} diff --git a/ante/evm/suite_test.go b/ante/evm/suite_test.go new file mode 100644 index 0000000..fc2e353 --- /dev/null +++ b/ante/evm/suite_test.go @@ -0,0 +1,30 @@ +package evm_test + +import ( + "testing" + + gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/suite" +) + +// EvmAnteTestSuite aims to test all EVM ante handler unit functions. +// NOTE: the suite only holds properties related to global execution parameters +// (what type of tx to run the tests with) not independent tests values. +type EvmAnteTestSuite struct { + suite.Suite + + // To make sure that every tests is run with all the tx types + ethTxType int +} + +func TestEvmAnteTestSuite(t *testing.T) { + suite.Run(t, &EvmAnteTestSuite{ + ethTxType: gethtypes.DynamicFeeTxType, + }) + suite.Run(t, &EvmAnteTestSuite{ + ethTxType: gethtypes.LegacyTxType, + }) + suite.Run(t, &EvmAnteTestSuite{ + ethTxType: gethtypes.AccessListTxType, + }) +} diff --git a/ante/evm/utils.go b/ante/evm/utils.go new file mode 100644 index 0000000..862d08c --- /dev/null +++ b/ante/evm/utils.go @@ -0,0 +1,77 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package evm + +import ( + "math" + "math/big" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + anteinterfaces "github.com/evmos/os/ante/interfaces" +) + +// DecoratorUtils contain a bunch of relevant variables used for a variety of checks +// throughout the verification of an Ethereum transaction. +type DecoratorUtils struct { + EvmParams evmtypes.Params + EthConfig *params.ChainConfig + Rules params.Rules + Signer ethtypes.Signer + BaseFee *big.Int + EvmDenom string + MempoolMinGasPrice sdkmath.LegacyDec + GlobalMinGasPrice sdkmath.LegacyDec + BlockTxIndex uint64 + TxGasLimit uint64 + GasWanted uint64 + MinPriority int64 + TxFee sdk.Coins +} + +// NewMonoDecoratorUtils returns a new DecoratorUtils instance. +// +// These utilities are extracted once at the beginning of the ante handle process, +// and are used throughout the entire decorator chain. +// This avoids redundant calls to the keeper and thus improves speed of transaction processing. +func NewMonoDecoratorUtils( + ctx sdk.Context, + ek anteinterfaces.EVMKeeper, + fmk anteinterfaces.FeeMarketKeeper, +) (*DecoratorUtils, error) { + evmParams := ek.GetParams(ctx) + chainCfg := evmParams.GetChainConfig() + ethCfg := chainCfg.EthereumConfig(ek.ChainID()) + blockHeight := big.NewInt(ctx.BlockHeight()) + rules := ethCfg.Rules(blockHeight, true) + baseFee := ek.GetBaseFee(ctx, ethCfg) + feeMarketParams := fmk.GetParams(ctx) + + if rules.IsLondon && baseFee == nil { + return nil, errorsmod.Wrap( + evmtypes.ErrInvalidBaseFee, + "base fee is supported but evm block context value is nil", + ) + } + + return &DecoratorUtils{ + EvmParams: evmParams, + EthConfig: ethCfg, + Rules: rules, + Signer: ethtypes.MakeSigner(ethCfg, blockHeight), + BaseFee: baseFee, + MempoolMinGasPrice: ctx.MinGasPrices().AmountOf(evmParams.EvmDenom), + GlobalMinGasPrice: feeMarketParams.MinGasPrice, + EvmDenom: evmParams.EvmDenom, + BlockTxIndex: ek.GetTxIndexTransient(ctx), + TxGasLimit: 0, + GasWanted: 0, + MinPriority: int64(math.MaxInt64), + TxFee: sdk.Coins{}, + }, nil +} diff --git a/ante/evm/utils_test.go b/ante/evm/utils_test.go new file mode 100644 index 0000000..4415ed6 --- /dev/null +++ b/ante/evm/utils_test.go @@ -0,0 +1,650 @@ +package evm_test + +import ( + "encoding/json" + "fmt" + "math/big" + "time" + + "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + kmultisig "github.com/cosmos/cosmos-sdk/crypto/keys/multisig" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/crypto/types/multisig" + sdk "github.com/cosmos/cosmos-sdk/types" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + sdkante "github.com/cosmos/cosmos-sdk/x/auth/ante" + "github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + authz "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + evtypes "github.com/cosmos/cosmos-sdk/x/evidence/types" + "github.com/cosmos/cosmos-sdk/x/feegrant" + govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + ibctypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + "github.com/evmos/os/crypto/ethsecp256k1" + "github.com/evmos/os/ethereum/eip712" + utiltx "github.com/evmos/os/testutil/tx" +) + +func (suite *AnteTestSuite) BuildTestEthTx( + from common.Address, + to common.Address, + amount *big.Int, + input []byte, + gasPrice *big.Int, + gasFeeCap *big.Int, + gasTipCap *big.Int, + accesses *ethtypes.AccessList, +) *evmtypes.MsgEthereumTx { + chainID := suite.app.EVMKeeper.ChainID() + nonce := suite.app.EVMKeeper.GetNonce( + suite.ctx, + common.BytesToAddress(from.Bytes()), + ) + + ethTxParams := &evmtypes.EvmTxArgs{ + ChainID: chainID, + Nonce: nonce, + To: &to, + Amount: amount, + GasLimit: TestGasLimit, + GasPrice: gasPrice, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + Input: input, + Accesses: accesses, + } + + msgEthereumTx := evmtypes.NewTx(ethTxParams) + msgEthereumTx.From = from.String() + return msgEthereumTx +} + +// CreateTestTx is a helper function to create a tx given multiple inputs. +// +//nolint:revive +func (suite *AnteTestSuite) CreateTestTx( + msg *evmtypes.MsgEthereumTx, priv cryptotypes.PrivKey, accNum uint64, signCosmosTx bool, + unsetExtensionOptions ...bool, +) authsigning.Tx { + return suite.CreateTestTxBuilder(msg, priv, accNum, signCosmosTx).GetTx() +} + +// CreateTestTxBuilder is a helper function to create a tx builder given multiple inputs. +func (suite *AnteTestSuite) CreateTestTxBuilder( + msg *evmtypes.MsgEthereumTx, priv cryptotypes.PrivKey, accNum uint64, signCosmosTx bool, + unsetExtensionOptions ...bool, +) client.TxBuilder { + var option *codectypes.Any + var err error + if len(unsetExtensionOptions) == 0 { + option, err = codectypes.NewAnyWithValue(&evmtypes.ExtensionOptionsEthereumTx{}) + suite.Require().NoError(err) + } + + txBuilder := suite.clientCtx.TxConfig.NewTxBuilder() + builder, ok := txBuilder.(authtx.ExtensionOptionsTxBuilder) + suite.Require().True(ok) + + if len(unsetExtensionOptions) == 0 { + builder.SetExtensionOptions(option) + } + + err = msg.Sign(suite.ethSigner, utiltx.NewSigner(priv)) + suite.Require().NoError(err) + + msg.From = "" + err = builder.SetMsgs(msg) + suite.Require().NoError(err) + + txData, err := evmtypes.UnpackTxData(msg.Data) + suite.Require().NoError(err) + + fees := sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewIntFromBigInt(txData.Fee()))) + builder.SetFeeAmount(fees) + builder.SetGasLimit(msg.GetGas()) + + if signCosmosTx { + // First round: we gather all the signer infos. We use the "set empty + // signature" hack to do that. + sigV2 := signing.SignatureV2{ + PubKey: priv.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: suite.clientCtx.TxConfig.SignModeHandler().DefaultMode(), + Signature: nil, + }, + Sequence: txData.GetNonce(), + } + + sigsV2 := []signing.SignatureV2{sigV2} + + err = txBuilder.SetSignatures(sigsV2...) + suite.Require().NoError(err) + + // Second round: all signer infos are set, so each signer can sign. + + signerData := authsigning.SignerData{ + ChainID: suite.ctx.ChainID(), + AccountNumber: accNum, + Sequence: txData.GetNonce(), + } + sigV2, err = tx.SignWithPrivKey( + suite.clientCtx.TxConfig.SignModeHandler().DefaultMode(), signerData, + txBuilder, priv, suite.clientCtx.TxConfig, txData.GetNonce(), + ) + suite.Require().NoError(err) + + sigsV2 = []signing.SignatureV2{sigV2} + + err = txBuilder.SetSignatures(sigsV2...) + suite.Require().NoError(err) + } + + return txBuilder +} + +func (suite *AnteTestSuite) RequireErrorForLegacyTypedData(err error) { + if suite.useLegacyEIP712TypedData { + suite.Require().Error(err) + } else { + suite.Require().NoError(err) + } +} + +func (suite *AnteTestSuite) TxForLegacyTypedData(txBuilder client.TxBuilder) sdk.Tx { + if suite.useLegacyEIP712TypedData { + // Since the TxBuilder will be nil on failure, + // we return an empty Tx to avoid panics. + emptyTxBuilder := suite.clientCtx.TxConfig.NewTxBuilder() + return emptyTxBuilder.GetTx() + } + + return txBuilder.GetTx() +} + +func (suite *AnteTestSuite) CreateTestCosmosTxBuilder(gasPrice math.Int, denom string, msgs ...sdk.Msg) client.TxBuilder { + txBuilder := suite.clientCtx.TxConfig.NewTxBuilder() + + txBuilder.SetGasLimit(TestGasLimit) + fees := &sdk.Coins{{Denom: denom, Amount: gasPrice.MulRaw(int64(TestGasLimit))}} + txBuilder.SetFeeAmount(*fees) + err := txBuilder.SetMsgs(msgs...) + suite.Require().NoError(err) + return txBuilder +} + +func (suite *AnteTestSuite) CreateTestEIP712TxBuilderMsgSend(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + // Build MsgSend + recipient := sdk.AccAddress(common.Address{}.Bytes()) + msgSend := banktypes.NewMsgSend(from, recipient, sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(1)))) + return suite.CreateTestEIP712SingleMessageTxBuilder(priv, chainID, gas, gasAmount, msgSend) +} + +func (suite *AnteTestSuite) CreateTestEIP712TxBuilderMsgDelegate(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + // Build MsgSend + valEthAddr := utiltx.GenerateAddress() + valAddr := sdk.ValAddress(valEthAddr.Bytes()) + msgSend := stakingtypes.NewMsgDelegate(from, valAddr, sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(20))) + return suite.CreateTestEIP712SingleMessageTxBuilder(priv, chainID, gas, gasAmount, msgSend) +} + +func (suite *AnteTestSuite) CreateTestEIP712MsgCreateValidator(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + // Build MsgCreateValidator + valAddr := sdk.ValAddress(from.Bytes()) + privEd := ed25519.GenPrivKey() + msgCreate, err := stakingtypes.NewMsgCreateValidator( + valAddr, + privEd.PubKey(), + sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(20)), + stakingtypes.NewDescription("moniker", "identity", "website", "security_contract", "details"), + stakingtypes.NewCommissionRates(math.LegacyOneDec(), math.LegacyOneDec(), math.LegacyOneDec()), + math.OneInt(), + ) + suite.Require().NoError(err) + return suite.CreateTestEIP712SingleMessageTxBuilder(priv, chainID, gas, gasAmount, msgCreate) +} + +func (suite *AnteTestSuite) CreateTestEIP712MsgCreateValidator2(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + // Build MsgCreateValidator + valAddr := sdk.ValAddress(from.Bytes()) + privEd := ed25519.GenPrivKey() + msgCreate, err := stakingtypes.NewMsgCreateValidator( + valAddr, + privEd.PubKey(), + sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(20)), + // Ensure optional fields can be left blank + stakingtypes.NewDescription("moniker", "identity", "", "", ""), + stakingtypes.NewCommissionRates(math.LegacyOneDec(), math.LegacyOneDec(), math.LegacyOneDec()), + math.OneInt(), + ) + suite.Require().NoError(err) + return suite.CreateTestEIP712SingleMessageTxBuilder(priv, chainID, gas, gasAmount, msgCreate) +} + +func (suite *AnteTestSuite) CreateTestEIP712SubmitProposal(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins, deposit sdk.Coins) (client.TxBuilder, error) { + proposal, ok := govtypes.ContentFromProposalType("My proposal", "My description", govtypes.ProposalTypeText) + suite.Require().True(ok) + msgSubmit, err := govtypes.NewMsgSubmitProposal(proposal, deposit, from) + suite.Require().NoError(err) + return suite.CreateTestEIP712SingleMessageTxBuilder(priv, chainID, gas, gasAmount, msgSubmit) +} + +func (suite *AnteTestSuite) CreateTestEIP712GrantAllowance(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + spendLimit := sdk.NewCoins(sdk.NewInt64Coin(evmtypes.DefaultEVMDenom, 10)) + threeHours := time.Now().Add(3 * time.Hour) + basic := &feegrant.BasicAllowance{ + SpendLimit: spendLimit, + Expiration: &threeHours, + } + granted := utiltx.GenerateAddress() + grantedAddr := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, granted.Bytes()) + msgGrant, err := feegrant.NewMsgGrantAllowance(basic, from, grantedAddr.GetAddress()) + suite.Require().NoError(err) + return suite.CreateTestEIP712SingleMessageTxBuilder(priv, chainID, gas, gasAmount, msgGrant) +} + +func (suite *AnteTestSuite) CreateTestEIP712MsgEditValidator(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + valAddr := sdk.ValAddress(from.Bytes()) + msgEdit := stakingtypes.NewMsgEditValidator( + valAddr, + stakingtypes.NewDescription("moniker", "identity", "website", "security_contract", "details"), + nil, + nil, + ) + return suite.CreateTestEIP712SingleMessageTxBuilder(priv, chainID, gas, gasAmount, msgEdit) +} + +func (suite *AnteTestSuite) CreateTestEIP712MsgSubmitEvidence(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + pk := ed25519.GenPrivKey() + msgEvidence, err := evtypes.NewMsgSubmitEvidence(from, &evtypes.Equivocation{ + Height: 11, + Time: time.Now().UTC(), + Power: 100, + ConsensusAddress: pk.PubKey().Address().String(), + }) + suite.Require().NoError(err) + + return suite.CreateTestEIP712SingleMessageTxBuilder(priv, chainID, gas, gasAmount, msgEvidence) +} + +func (suite *AnteTestSuite) CreateTestEIP712MsgVoteV1(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + msgVote := govtypesv1.NewMsgVote(from, 1, govtypesv1.VoteOption_VOTE_OPTION_YES, "") + return suite.CreateTestEIP712SingleMessageTxBuilder(priv, chainID, gas, gasAmount, msgVote) +} + +func (suite *AnteTestSuite) CreateTestEIP712SubmitProposalV1(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + // Build V1 proposal messages. Must all be same-type, since EIP-712 + // does not support arrays of variable type. + authAcc := suite.app.GovKeeper.GetGovernanceAccount(suite.ctx) + + proposal1, ok := govtypes.ContentFromProposalType("My proposal 1", "My description 1", govtypes.ProposalTypeText) + suite.Require().True(ok) + content1, err := govtypesv1.NewLegacyContent( + proposal1, + sdk.MustBech32ifyAddressBytes(sdk.GetConfig().GetBech32AccountAddrPrefix(), authAcc.GetAddress().Bytes()), + ) + suite.Require().NoError(err) + + proposal2, ok := govtypes.ContentFromProposalType("My proposal 2", "My description 2", govtypes.ProposalTypeText) + suite.Require().True(ok) + content2, err := govtypesv1.NewLegacyContent( + proposal2, + sdk.MustBech32ifyAddressBytes(sdk.GetConfig().GetBech32AccountAddrPrefix(), authAcc.GetAddress().Bytes()), + ) + suite.Require().NoError(err) + + proposalMsgs := []sdk.Msg{ + content1, + content2, + } + + // Build V1 proposal + msgProposal, err := govtypesv1.NewMsgSubmitProposal( + proposalMsgs, + sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(100))), + sdk.MustBech32ifyAddressBytes(sdk.GetConfig().GetBech32AccountAddrPrefix(), from.Bytes()), + "Metadata", "title", "summary", + ) + + suite.Require().NoError(err) + + return suite.CreateTestEIP712SingleMessageTxBuilder(priv, chainID, gas, gasAmount, msgProposal) +} + +func (suite *AnteTestSuite) CreateTestEIP712MsgExec(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + recipient := sdk.AccAddress(common.Address{}.Bytes()) + msgSend := banktypes.NewMsgSend(from, recipient, sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(1)))) + msgExec := authz.NewMsgExec(from, []sdk.Msg{msgSend}) + return suite.CreateTestEIP712SingleMessageTxBuilder(priv, chainID, gas, gasAmount, &msgExec) +} + +func (suite *AnteTestSuite) CreateTestEIP712MultipleMsgSend(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + recipient := sdk.AccAddress(common.Address{}.Bytes()) + msgSend := banktypes.NewMsgSend(from, recipient, sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(1)))) + return suite.CreateTestEIP712CosmosTxBuilder(priv, chainID, gas, gasAmount, []sdk.Msg{msgSend, msgSend, msgSend}) +} + +func (suite *AnteTestSuite) CreateTestEIP712MultipleDifferentMsgs(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + recipient := sdk.AccAddress(common.Address{}.Bytes()) + msgSend := banktypes.NewMsgSend(from, recipient, sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(1)))) + + msgVote := govtypesv1.NewMsgVote(from, 1, govtypesv1.VoteOption_VOTE_OPTION_YES, "") + + valEthAddr := utiltx.GenerateAddress() + valAddr := sdk.ValAddress(valEthAddr.Bytes()) + msgDelegate := stakingtypes.NewMsgDelegate(from, valAddr, sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(20))) + + return suite.CreateTestEIP712CosmosTxBuilder(priv, chainID, gas, gasAmount, []sdk.Msg{msgSend, msgVote, msgDelegate}) +} + +func (suite *AnteTestSuite) CreateTestEIP712SameMsgDifferentSchemas(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + msgVote1 := govtypesv1.NewMsgVote(from, 1, govtypesv1.VoteOption_VOTE_OPTION_YES, "") + msgVote2 := govtypesv1.NewMsgVote(from, 5, govtypesv1.VoteOption_VOTE_OPTION_ABSTAIN, "With Metadata") + + return suite.CreateTestEIP712CosmosTxBuilder(priv, chainID, gas, gasAmount, []sdk.Msg{msgVote1, msgVote2}) +} + +func (suite *AnteTestSuite) CreateTestEIP712ZeroValueArray(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + recipient := sdk.AccAddress(common.Address{}.Bytes()) + msgSend := banktypes.NewMsgSend(from, recipient, sdk.NewCoins()) + return suite.CreateTestEIP712CosmosTxBuilder(priv, chainID, gas, gasAmount, []sdk.Msg{msgSend}) +} + +func (suite *AnteTestSuite) CreateTestEIP712ZeroValueNumber(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + msgVote := govtypesv1.NewMsgVote(from, 0, govtypesv1.VoteOption_VOTE_OPTION_NO, "") + + return suite.CreateTestEIP712CosmosTxBuilder(priv, chainID, gas, gasAmount, []sdk.Msg{msgVote}) +} + +func (suite *AnteTestSuite) CreateTestEIP712MsgTransfer(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + msgTransfer := suite.createMsgTransfer(from, "With Memo") + return suite.CreateTestEIP712SingleMessageTxBuilder(priv, chainID, gas, gasAmount, msgTransfer) +} + +func (suite *AnteTestSuite) CreateTestEIP712MsgTransferWithoutMemo(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + msgTransfer := suite.createMsgTransfer(from, "") + return suite.CreateTestEIP712SingleMessageTxBuilder(priv, chainID, gas, gasAmount, msgTransfer) +} + +func (suite *AnteTestSuite) createMsgTransfer(from sdk.AccAddress, memo string) *ibctypes.MsgTransfer { + recipient := sdk.AccAddress(common.Address{}.Bytes()) + msgTransfer := ibctypes.NewMsgTransfer("transfer", "channel-25", sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(100000)), from.String(), recipient.String(), ibcclienttypes.NewHeight(1000, 1000), 1000, memo) + return msgTransfer +} + +func (suite *AnteTestSuite) CreateTestEIP712MultipleSignerMsgs(from sdk.AccAddress, priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins) (client.TxBuilder, error) { + recipient := sdk.AccAddress(common.Address{}.Bytes()) + msgSend1 := banktypes.NewMsgSend(from, recipient, sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(1)))) + msgSend2 := banktypes.NewMsgSend(recipient, from, sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(1)))) + return suite.CreateTestEIP712CosmosTxBuilder(priv, chainID, gas, gasAmount, []sdk.Msg{msgSend1, msgSend2}) +} + +// StdSignBytes returns the bytes to sign for a transaction. +func StdSignBytes(cdc *codec.LegacyAmino, chainID string, accnum uint64, sequence uint64, timeout uint64, fee legacytx.StdFee, msgs []sdk.Msg, memo string, tip *txtypes.Tip) []byte { + msgsBytes := make([]json.RawMessage, 0, len(msgs)) + for _, msg := range msgs { + legacyMsg, ok := msg.(legacytx.LegacyMsg) + if !ok { + panic(fmt.Errorf("expected %T when using amino JSON", (*legacytx.LegacyMsg)(nil))) + } + + msgsBytes = append(msgsBytes, json.RawMessage(legacyMsg.GetSignBytes())) + } + + var stdTip *legacytx.StdTip + if tip != nil { + if tip.Tipper == "" { + panic(fmt.Errorf("tipper cannot be empty")) + } + + stdTip = &legacytx.StdTip{Amount: tip.Amount, Tipper: tip.Tipper} + } + + bz, err := cdc.MarshalJSON(legacytx.StdSignDoc{ + AccountNumber: accnum, + ChainID: chainID, + Fee: json.RawMessage(fee.Bytes()), + Memo: memo, + Msgs: msgsBytes, + Sequence: sequence, + TimeoutHeight: timeout, + Tip: stdTip, + }) + if err != nil { + panic(err) + } + + return sdk.MustSortJSON(bz) +} + +func (suite *AnteTestSuite) CreateTestEIP712SingleMessageTxBuilder( + priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins, msg sdk.Msg, +) (client.TxBuilder, error) { + msgs := []sdk.Msg{msg} + return suite.CreateTestEIP712CosmosTxBuilder( + priv, + chainID, + gas, + gasAmount, + msgs, + ) +} + +func (suite *AnteTestSuite) CreateTestEIP712CosmosTxBuilder( + priv cryptotypes.PrivKey, chainID string, gas uint64, gasAmount sdk.Coins, msgs []sdk.Msg, +) (client.TxBuilder, error) { + txConf := suite.clientCtx.TxConfig + cosmosTxArgs := utiltx.CosmosTxArgs{ + TxCfg: txConf, + Priv: priv, + ChainID: chainID, + Gas: gas, + Fees: gasAmount, + Msgs: msgs, + } + + return utiltx.PrepareEIP712CosmosTx( + suite.ctx, + suite.app, + utiltx.EIP712TxArgs{ + CosmosTxArgs: cosmosTxArgs, + UseLegacyTypedData: suite.useLegacyEIP712TypedData, + }, + ) +} + +// Generate a set of pub/priv keys to be used in creating multi-keys +func (suite *AnteTestSuite) GenerateMultipleKeys(n int) ([]cryptotypes.PrivKey, []cryptotypes.PubKey) { + privKeys := make([]cryptotypes.PrivKey, n) + pubKeys := make([]cryptotypes.PubKey, n) + for i := 0; i < n; i++ { + privKey, err := ethsecp256k1.GenerateKey() + suite.Require().NoError(err) + privKeys[i] = privKey + pubKeys[i] = privKey.PubKey() + } + return privKeys, pubKeys +} + +// generateSingleSignature signs the given sign doc bytes using the given signType (EIP-712 or Standard) +func (suite *AnteTestSuite) generateSingleSignature(signMode signing.SignMode, privKey cryptotypes.PrivKey, signDocBytes []byte, signType string) (signature signing.SignatureV2) { + var ( + msg []byte + err error + ) + + msg = signDocBytes + + if signType == "EIP-712" { + msg, err = eip712.GetEIP712BytesForMsg(signDocBytes) + suite.Require().NoError(err) + } + + sigBytes, _ := privKey.Sign(msg) + sigData := &signing.SingleSignatureData{ + SignMode: signMode, + Signature: sigBytes, + } + + return signing.SignatureV2{ + PubKey: privKey.PubKey(), + Data: sigData, + } +} + +// generateMultikeySignatures signs a set of messages using each private key within a given multi-key +func (suite *AnteTestSuite) generateMultikeySignatures(signMode signing.SignMode, privKeys []cryptotypes.PrivKey, signDocBytes []byte, signType string) (signatures []signing.SignatureV2) { + n := len(privKeys) + signatures = make([]signing.SignatureV2, n) + + for i := 0; i < n; i++ { + privKey := privKeys[i] + currentType := signType + + // If mixed type, alternate signing type on each iteration + if signType == "mixed" { + if i%2 == 0 { + currentType = "EIP-712" + } else { + currentType = "Standard" + } + } + + signatures[i] = suite.generateSingleSignature( + signMode, + privKey, + signDocBytes, + currentType, + ) + } + + return signatures +} + +// RegisterAccount creates an account with the keeper and populates the initial balance +func (suite *AnteTestSuite) RegisterAccount(pubKey cryptotypes.PubKey, balance *big.Int) { + acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, sdk.AccAddress(pubKey.Address())) + suite.app.AccountKeeper.SetAccount(suite.ctx, acc) + + err := suite.app.EVMKeeper.SetBalance(suite.ctx, common.BytesToAddress(pubKey.Address()), balance) + suite.Require().NoError(err) +} + +// createSignerBytes generates sign doc bytes using the given parameters +func (suite *AnteTestSuite) createSignerBytes(chainID string, signMode signing.SignMode, pubKey cryptotypes.PubKey, txBuilder client.TxBuilder) []byte { + acc, err := sdkante.GetSignerAcc(suite.ctx, suite.app.AccountKeeper, sdk.AccAddress(pubKey.Address())) + suite.Require().NoError(err) + signerInfo := authsigning.SignerData{ + Address: sdk.MustBech32ifyAddressBytes(sdk.GetConfig().GetBech32AccountAddrPrefix(), acc.GetAddress().Bytes()), + ChainID: chainID, + AccountNumber: acc.GetAccountNumber(), + Sequence: acc.GetSequence(), + PubKey: pubKey, + } + + signerBytes, err := suite.clientCtx.TxConfig.SignModeHandler().GetSignBytes( + signMode, + signerInfo, + txBuilder.GetTx(), + ) + suite.Require().NoError(err) + + return signerBytes +} + +// createBaseTxBuilder creates a TxBuilder to be used for Single- or Multi-signing +func (suite *AnteTestSuite) createBaseTxBuilder(msg sdk.Msg, gas uint64) client.TxBuilder { + txBuilder := suite.clientCtx.TxConfig.NewTxBuilder() + + txBuilder.SetGasLimit(gas) + txBuilder.SetFeeAmount(sdk.NewCoins( + sdk.NewCoin(evmtypes.DefaultEVMDenom, math.NewInt(10000)), + )) + + err := txBuilder.SetMsgs(msg) + suite.Require().NoError(err) + + txBuilder.SetMemo("") + + return txBuilder +} + +// CreateTestSignedMultisigTx creates and sign a multi-signed tx for the given message. `signType` indicates whether to use standard signing ("Standard"), +// EIP-712 signing ("EIP-712"), or a mix of the two ("mixed"). +func (suite *AnteTestSuite) CreateTestSignedMultisigTx(privKeys []cryptotypes.PrivKey, signMode signing.SignMode, msg sdk.Msg, chainID string, gas uint64, signType string) client.TxBuilder { + pubKeys := make([]cryptotypes.PubKey, len(privKeys)) + for i, privKey := range privKeys { + pubKeys[i] = privKey.PubKey() + } + + // Re-derive multikey + numKeys := len(privKeys) + multiKey := kmultisig.NewLegacyAminoPubKey(numKeys, pubKeys) + + suite.RegisterAccount(multiKey, big.NewInt(10000000000)) + + txBuilder := suite.createBaseTxBuilder(msg, gas) + + // Prepare signature field + sig := multisig.NewMultisig(len(pubKeys)) + err := txBuilder.SetSignatures(signing.SignatureV2{ + PubKey: multiKey, + Data: sig, + }) + suite.Require().NoError(err) + + signerBytes := suite.createSignerBytes(chainID, signMode, multiKey, txBuilder) + + // Sign for each key and update signature field + sigs := suite.generateMultikeySignatures(signMode, privKeys, signerBytes, signType) + for _, pkSig := range sigs { + err := multisig.AddSignatureV2(sig, pkSig, pubKeys) + suite.Require().NoError(err) + } + + err = txBuilder.SetSignatures(signing.SignatureV2{ + PubKey: multiKey, + Data: sig, + }) + suite.Require().NoError(err) + + return txBuilder +} + +func (suite *AnteTestSuite) CreateTestSingleSignedTx(privKey cryptotypes.PrivKey, signMode signing.SignMode, msg sdk.Msg, chainID string, gas uint64, signType string) client.TxBuilder { + pubKey := privKey.PubKey() + + suite.RegisterAccount(pubKey, big.NewInt(10000000000)) + + txBuilder := suite.createBaseTxBuilder(msg, gas) + + // Prepare signature field + sig := signing.SingleSignatureData{} + err := txBuilder.SetSignatures(signing.SignatureV2{ + PubKey: pubKey, + Data: &sig, + }) + suite.Require().NoError(err) + + signerBytes := suite.createSignerBytes(chainID, signMode, pubKey, txBuilder) + + sigData := suite.generateSingleSignature(signMode, privKey, signerBytes, signType) + err = txBuilder.SetSignatures(sigData) + suite.Require().NoError(err) + + return txBuilder +} diff --git a/ante/interfaces/cosmos.go b/ante/interfaces/cosmos.go new file mode 100644 index 0000000..c100460 --- /dev/null +++ b/ante/interfaces/cosmos.go @@ -0,0 +1,28 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package interfaces + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// BankKeeper defines the exposed interface for using functionality of the bank keeper +// in the context of the AnteHandler utils package. +type BankKeeper interface { + GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin +} + +// DistributionKeeper defines the exposed interface for using functionality of the distribution +// keeper in the context of the AnteHandler utils package. +type DistributionKeeper interface { + WithdrawDelegationRewards(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) (sdk.Coins, error) +} + +// StakingKeeper defines the exposed interface for using functionality of the staking keeper +// in the context of the AnteHandler utils package. +type StakingKeeper interface { + BondDenom(ctx sdk.Context) string + IterateDelegations(ctx sdk.Context, delegator sdk.AccAddress, fn func(index int64, delegation stakingtypes.DelegationI) (stop bool)) +} diff --git a/ante/interfaces/evm.go b/ante/interfaces/evm.go new file mode 100644 index 0000000..d864a08 --- /dev/null +++ b/ante/interfaces/evm.go @@ -0,0 +1,50 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package interfaces + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/params" + "github.com/evmos/evmos/v19/x/evm/core/vm" + + "github.com/evmos/evmos/v19/x/evm/statedb" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + feemarkettypes "github.com/evmos/evmos/v19/x/feemarket/types" +) + +// EVMKeeper exposes the required EVM keeper interface required for ante handlers +type EVMKeeper interface { //nolint: revive + statedb.Keeper + DynamicFeeEVMKeeper + + NewEVM(ctx sdk.Context, msg core.Message, cfg *statedb.EVMConfig, tracer vm.EVMLogger, stateDB vm.StateDB) *vm.EVM + DeductTxCostsFromUserBalance(ctx sdk.Context, fees sdk.Coins, from common.Address) error + GetBalance(ctx sdk.Context, addr common.Address) *big.Int + ResetTransientGasUsed(ctx sdk.Context) + GetTxIndexTransient(ctx sdk.Context) uint64 + GetParams(ctx sdk.Context) evmtypes.Params +} + +// FeeMarketKeeper exposes the required feemarket keeper interface required for ante handlers +type FeeMarketKeeper interface { + GetParams(ctx sdk.Context) (params feemarkettypes.Params) + AddTransientGasWanted(ctx sdk.Context, gasWanted uint64) (uint64, error) + GetBaseFeeEnabled(ctx sdk.Context) bool +} + +// DynamicFeeEVMKeeper is a subset of EVMKeeper interface that supports dynamic fee checker +type DynamicFeeEVMKeeper interface { + ChainID() *big.Int + GetParams(ctx sdk.Context) evmtypes.Params + GetBaseFee(ctx sdk.Context, ethCfg *params.ChainConfig) *big.Int +} + +type ProtoTxProvider interface { + GetProtoTx() *tx.Tx +} diff --git a/ante/sigverify.go b/ante/sigverify.go new file mode 100644 index 0000000..de94707 --- /dev/null +++ b/ante/sigverify.go @@ -0,0 +1,90 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package ante + +import ( + "fmt" + + errorsmod "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + "github.com/cosmos/cosmos-sdk/crypto/types/multisig" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + "github.com/evmos/os/crypto/ethsecp256k1" +) + +var _ authante.SignatureVerificationGasConsumer = SigVerificationGasConsumer + +const ( + Secp256k1VerifyCost uint64 = 21000 +) + +// SigVerificationGasConsumer is the evmOS implementation of SignatureVerificationGasConsumer. It consumes gas +// for signature verification based upon the public key type. The cost is fetched from the given params and is matched +// by the concrete type. +// The types of keys supported are: +// +// - ethsecp256k1 (Ethereum keys) +// +// - ed25519 (Validators) +// +// - multisig (Cosmos SDK multisigs) +func SigVerificationGasConsumer( + meter storetypes.GasMeter, sig signing.SignatureV2, params authtypes.Params, +) error { + pubkey := sig.PubKey + switch pubkey := pubkey.(type) { + + case *ethsecp256k1.PubKey: + // Ethereum keys + meter.ConsumeGas(Secp256k1VerifyCost, "ante verify: eth_secp256k1") + return nil + case *ed25519.PubKey: + // Validator keys + meter.ConsumeGas(params.SigVerifyCostED25519, "ante verify: ed25519") + return errorsmod.Wrap(errortypes.ErrInvalidPubKey, "ED25519 public keys are unsupported") + + case multisig.PubKey: + // Multisig keys + multisignature, ok := sig.Data.(*signing.MultiSignatureData) + if !ok { + return fmt.Errorf("expected %T, got, %T", &signing.MultiSignatureData{}, sig.Data) + } + return ConsumeMultisignatureVerificationGas(meter, multisignature, pubkey, params, sig.Sequence) + + default: + return errorsmod.Wrapf(errortypes.ErrInvalidPubKey, "unrecognized/unsupported public key type: %T", pubkey) + } +} + +// ConsumeMultisignatureVerificationGas consumes gas from a GasMeter for verifying a multisig pubkey signature +func ConsumeMultisignatureVerificationGas( + meter storetypes.GasMeter, sig *signing.MultiSignatureData, pubkey multisig.PubKey, + params authtypes.Params, accSeq uint64, +) error { + size := sig.BitArray.Count() + sigIndex := 0 + + for i := 0; i < size; i++ { + if !sig.BitArray.GetIndex(i) { + continue + } + sigV2 := signing.SignatureV2{ + PubKey: pubkey.GetPubKeys()[i], + Data: sig.Signatures[sigIndex], + Sequence: accSeq, + } + err := SigVerificationGasConsumer(meter, sigV2, params) + if err != nil { + return err + } + sigIndex++ + } + + return nil +} diff --git a/ante/sigverify_test.go b/ante/sigverify_test.go new file mode 100644 index 0000000..8cd669c --- /dev/null +++ b/ante/sigverify_test.go @@ -0,0 +1,115 @@ +package ante_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + kmultisig "github.com/cosmos/cosmos-sdk/crypto/keys/multisig" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256r1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/crypto/types/multisig" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + "github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + "github.com/evmos/os/ante" + "github.com/evmos/os/crypto/ethsecp256k1" + "github.com/evmos/os/encoding" + app "github.com/evmos/os/example_chain" +) + +func TestConsumeSignatureVerificationGas(t *testing.T) { + params := authtypes.DefaultParams() + msg := []byte{1, 2, 3, 4} + + encodingConfig := encoding.MakeConfig(app.ModuleBasics) + cdc := encodingConfig.Amino + + p := authtypes.DefaultParams() + pkSet1, sigSet1 := generatePubKeysAndSignatures(5, msg, false) + multisigKey1 := kmultisig.NewLegacyAminoPubKey(2, pkSet1) + multisignature1 := multisig.NewMultisig(len(pkSet1)) + expectedCost1 := expectedGasCostByKeys(pkSet1) + + for i := 0; i < len(pkSet1); i++ { + // using nolint:all because the staticcheck nolint is not working as expected + stdSig := legacytx.StdSignature{PubKey: pkSet1[i], Signature: sigSet1[i]} //nolint:all + sigV2, err := legacytx.StdSignatureToSignatureV2(cdc, stdSig) + require.NoError(t, err) + err = multisig.AddSignatureV2(multisignature1, sigV2, pkSet1) + require.NoError(t, err) + } + + ethsecKey, _ := ethsecp256k1.GenerateKey() + skR1, _ := secp256r1.GenPrivKey() + + type args struct { + meter storetypes.GasMeter + sig signing.SignatureData + pubkey cryptotypes.PubKey + params authtypes.Params + } + tests := []struct { + name string + args args + gasConsumed uint64 + shouldErr bool + }{ + { + "PubKeyEd25519", + args{sdk.NewInfiniteGasMeter(), nil, ed25519.GenPrivKey().PubKey(), params}, + p.SigVerifyCostED25519, + true, + }, + { + "PubKeyEthsecp256k1", + args{sdk.NewInfiniteGasMeter(), nil, ethsecKey.PubKey(), params}, + ante.Secp256k1VerifyCost, + false, + }, + { + "PubKeySecp256k1", + args{sdk.NewInfiniteGasMeter(), nil, secp256k1.GenPrivKey().PubKey(), params}, + p.SigVerifyCostSecp256k1, + true, + }, + { + "PubKeySecp256r1", + args{sdk.NewInfiniteGasMeter(), nil, skR1.PubKey(), params}, + p.SigVerifyCostSecp256r1(), + true, + }, + { + "Multisig", + args{sdk.NewInfiniteGasMeter(), multisignature1, multisigKey1, params}, + expectedCost1, + false, + }, + { + "unknown key", + args{sdk.NewInfiniteGasMeter(), nil, nil, params}, + 0, + true, + }, + } + for _, tt := range tests { + sigV2 := signing.SignatureV2{ + PubKey: tt.args.pubkey, + Data: tt.args.sig, + Sequence: 0, // Arbitrary account sequence + } + err := ante.SigVerificationGasConsumer(tt.args.meter, sigV2, tt.args.params) + + if tt.shouldErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.gasConsumed, tt.args.meter.GasConsumed()) + } + } +} diff --git a/ante/utils_test.go b/ante/utils_test.go new file mode 100644 index 0000000..32fed94 --- /dev/null +++ b/ante/utils_test.go @@ -0,0 +1,38 @@ +package ante_test + +import ( + "fmt" + "strings" + + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/evmos/os/ante" + "github.com/evmos/os/crypto/ethsecp256k1" +) + +func generatePubKeysAndSignatures(n int, msg []byte, _ bool) (pubkeys []cryptotypes.PubKey, signatures [][]byte) { + pubkeys = make([]cryptotypes.PubKey, n) + signatures = make([][]byte, n) + for i := 0; i < n; i++ { + privkey, _ := ethsecp256k1.GenerateKey() + pubkeys[i] = privkey.PubKey() + signatures[i], _ = privkey.Sign(msg) + } + return +} + +func expectedGasCostByKeys(pubkeys []cryptotypes.PubKey) uint64 { + cost := uint64(0) + for _, pubkey := range pubkeys { + pubkeyType := strings.ToLower(fmt.Sprintf("%T", pubkey)) + switch { + case strings.Contains(pubkeyType, "ed25519"): + cost += authtypes.DefaultSigVerifyCostED25519 + case strings.Contains(pubkeyType, "ethsecp256k1"): + cost += ante.Secp256k1VerifyCost + default: + panic("unexpected key type") + } + } + return cost +} diff --git a/example_chain/ante/ante.go b/example_chain/ante/ante.go new file mode 100644 index 0000000..345a7c7 --- /dev/null +++ b/example_chain/ante/ante.go @@ -0,0 +1,55 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package ante + +import ( + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" +) + +// NewAnteHandler returns an ante handler responsible for attempting to route an +// Ethereum or SDK transaction to an internal ante handler for performing +// transaction-level processing (e.g. fee payment, signature verification) before +// being passed onto it's respective handler. +func NewAnteHandler(options HandlerOptions) sdk.AnteHandler { + return func( + ctx sdk.Context, tx sdk.Tx, sim bool, + ) (newCtx sdk.Context, err error) { + var anteHandler sdk.AnteHandler + + txWithExtensions, ok := tx.(authante.HasExtensionOptionsTx) + if ok { + opts := txWithExtensions.GetExtensionOptions() + if len(opts) > 0 { + switch typeURL := opts[0].GetTypeUrl(); typeURL { + case "/ethermint.evm.v1.ExtensionOptionsEthereumTx": + // handle as *evmtypes.MsgEthereumTx + anteHandler = newMonoEVMAnteHandler(options) + case "/ethermint.types.v1.ExtensionOptionDynamicFeeTx": + // cosmos-sdk tx with dynamic fee extension + anteHandler = newCosmosAnteHandler(options) + default: + return ctx, errorsmod.Wrapf( + errortypes.ErrUnknownExtensionOptions, + "rejecting tx with unsupported extension option: %s", typeURL, + ) + } + + return anteHandler(ctx, tx, sim) + } + } + + // handle as totally normal Cosmos SDK tx + switch tx.(type) { + case sdk.Tx: + anteHandler = newCosmosAnteHandler(options) + default: + return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid transaction type: %T", tx) + } + + return anteHandler(ctx, tx, sim) + } +} diff --git a/example_chain/ante/cosmos_handler.go b/example_chain/ante/cosmos_handler.go new file mode 100644 index 0000000..f281c32 --- /dev/null +++ b/example_chain/ante/cosmos_handler.go @@ -0,0 +1,39 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package ante + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/ante" + sdkvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + evmoscosmosante "github.com/evmos/os/ante/cosmos" + evmante "github.com/evmos/os/ante/evm" +) + +// newCosmosAnteHandler creates the default ante handler for Cosmos transactions +func newCosmosAnteHandler(options HandlerOptions) sdk.AnteHandler { + return sdk.ChainAnteDecorators( + evmoscosmosante.RejectMessagesDecorator{}, // reject MsgEthereumTxs + evmoscosmosante.NewAuthzLimiterDecorator( // disable the Msg types that cannot be included on an authz.MsgExec msgs field + sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}), + sdk.MsgTypeURL(&sdkvesting.MsgCreateVestingAccount{}), + ), + ante.NewSetUpContextDecorator(), + ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker), + ante.NewValidateBasicDecorator(), + ante.NewTxTimeoutHeightDecorator(), + ante.NewValidateMemoDecorator(options.AccountKeeper), + evmoscosmosante.NewMinGasPriceDecorator(options.FeeMarketKeeper, options.EvmKeeper), + ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper), + ante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TxFeeChecker), + // SetPubKeyDecorator must be called before all signature verification decorators + ante.NewSetPubKeyDecorator(options.AccountKeeper), + ante.NewValidateSigCountDecorator(options.AccountKeeper), + ante.NewSigGasConsumeDecorator(options.AccountKeeper, options.SigGasConsumer), + ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler), + ante.NewIncrementSequenceDecorator(options.AccountKeeper), + evmante.NewGasWantedDecorator(options.EvmKeeper, options.FeeMarketKeeper), + ) +} diff --git a/example_chain/ante/evm_handler.go b/example_chain/ante/evm_handler.go new file mode 100644 index 0000000..2878614 --- /dev/null +++ b/example_chain/ante/evm_handler.go @@ -0,0 +1,281 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package ante + +import ( + "math/big" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + evmkeeper "github.com/evmos/evmos/v19/x/evm/keeper" + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + anteutils "github.com/evmos/os/ante/cosmos" + evmante "github.com/evmos/os/ante/evm" + anteinterfaces "github.com/evmos/os/ante/interfaces" +) + +// newMonoEVMAnteHandler creates the sdk.AnteHandler implementation for the EVM transactions. +func newMonoEVMAnteHandler(options HandlerOptions) sdk.AnteHandler { + return sdk.ChainAnteDecorators( + NewEVMMonoDecorator( + options.AccountKeeper, + options.BankKeeper, + options.FeeMarketKeeper, + options.EvmKeeper, + options.DistributionKeeper, + options.StakingKeeper, + options.MaxTxGasWanted, + ), + ) +} + +// MonoDecorator is a single decorator that handles all the prechecks for +// ethereum transactions. +type MonoDecorator struct { + accountKeeper evmtypes.AccountKeeper + bankKeeper evmtypes.BankKeeper + feeMarketKeeper anteinterfaces.FeeMarketKeeper + evmKeeper anteinterfaces.EVMKeeper + distributionKeeper anteutils.DistributionKeeper + stakingKeeper anteutils.StakingKeeper + maxGasWanted uint64 +} + +type DecoratorUtils struct { + EvmParams evmtypes.Params + EthConfig *params.ChainConfig + Rules params.Rules + Signer ethtypes.Signer + BaseFee *big.Int + EvmDenom string + MempoolMinGasPrice sdkmath.LegacyDec + GlobalMinGasPrice sdkmath.LegacyDec + BlockTxIndex uint64 + TxGasLimit uint64 + GasWanted uint64 + MinPriority int64 + TxFee sdk.Coins +} + +// MonoDecorator creates the 'mono' decorator, that is used to run the ante handle logic +// for EVM transactions on the chain. +func NewEVMMonoDecorator( + accountKeeper evmtypes.AccountKeeper, + bankKeeper evmtypes.BankKeeper, + feeMarketKeeper anteinterfaces.FeeMarketKeeper, + evmKeeper anteinterfaces.EVMKeeper, + distributionKeeper anteutils.DistributionKeeper, + stakingKeeper anteutils.StakingKeeper, + maxGasWanted uint64, +) MonoDecorator { + return MonoDecorator{ + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, + feeMarketKeeper: feeMarketKeeper, + evmKeeper: evmKeeper, + distributionKeeper: distributionKeeper, + stakingKeeper: stakingKeeper, + maxGasWanted: maxGasWanted, + } +} + +// AnteHandle handles the entire decorator chain using a mono decorator. +func (md MonoDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + // 0. Basic validation of the transaction + var txFeeInfo *txtypes.Fee + if !ctx.IsReCheckTx() { + txFeeInfo, err = evmante.ValidateTx(tx) + if err != nil { + return ctx, err + } + } + + // 1. setup ctx + ctx, err = evmante.SetupContext(ctx, tx, md.evmKeeper) + if err != nil { + return ctx, err + } + + // 2. get utils + decUtils, err := evmante.NewMonoDecoratorUtils(ctx, md.evmKeeper, md.feeMarketKeeper) + if err != nil { + return ctx, err + } + + // Use the lowest priority of all the messages as the final one. + for i, msg := range tx.GetMsgs() { + ethMsg, txData, from, err := evmtypes.UnpackEthMsg(msg) + if err != nil { + return ctx, err + } + + feeAmt := txData.Fee() + gas := txData.GetGas() + fee := sdkmath.LegacyNewDecFromBigInt(feeAmt) + gasLimit := sdkmath.LegacyNewDecFromBigInt(new(big.Int).SetUint64(gas)) + + // 2. mempool inclusion fee + if ctx.IsCheckTx() && !simulate { + if err := evmante.CheckMempoolFee(fee, decUtils.MempoolMinGasPrice, gasLimit, decUtils.Rules.IsLondon); err != nil { + return ctx, err + } + } + + // 3. min gas price (global min fee) + if txData.TxType() == ethtypes.DynamicFeeTxType && decUtils.BaseFee != nil { + feeAmt = txData.EffectiveFee(decUtils.BaseFee) + fee = sdkmath.LegacyNewDecFromBigInt(feeAmt) + } + + if err := evmante.CheckGlobalFee(fee, decUtils.GlobalMinGasPrice, gasLimit); err != nil { + return ctx, err + } + + // 4. validate msg contents + err = evmante.ValidateMsg( + decUtils.EvmParams, + txData, + from, + ) + if err != nil { + return ctx, err + } + + // 5. signature verification + if err := evmante.SignatureVerification( + ethMsg, + decUtils.Signer, + decUtils.EvmParams.AllowUnprotectedTxs, + ); err != nil { + return ctx, err + } + + // NOTE: sender address has been verified and cached + from = ethMsg.GetFrom() + + // 6. account balance verification + fromAddr := common.HexToAddress(ethMsg.From) + // TODO: Use account from AccountKeeper instead + account := md.evmKeeper.GetAccount(ctx, fromAddr) + if err := evmante.VerifyAccountBalance( + ctx, + md.accountKeeper, + account, + fromAddr, + txData, + ); err != nil { + return ctx, err + } + + // 7. can transfer + coreMsg, err := ethMsg.AsMessage(decUtils.Signer, decUtils.BaseFee) + if err != nil { + return ctx, errorsmod.Wrapf( + err, + "failed to create an ethereum core.Message from signer %T", decUtils.Signer, + ) + } + + if err := evmante.CanTransfer( + ctx, + md.evmKeeper, + coreMsg, + decUtils.BaseFee, + decUtils.EthConfig, + decUtils.EvmParams, + decUtils.Rules.IsLondon, + ); err != nil { + return ctx, err + } + + // 8. gas consumption + msgFees, err := evmkeeper.VerifyFee( + txData, + decUtils.EvmDenom, + decUtils.BaseFee, + decUtils.Rules.IsHomestead, + decUtils.Rules.IsIstanbul, + ctx.IsCheckTx(), + ) + if err != nil { + return ctx, err + } + + err = evmante.ConsumeFeesAndEmitEvent( + ctx, + &evmante.ConsumeGasKeepers{ + Bank: md.bankKeeper, + Distribution: md.distributionKeeper, + Evm: md.evmKeeper, + Staking: md.stakingKeeper, + }, + msgFees, + from, + ) + if err != nil { + return ctx, err + } + + gasWanted := evmante.UpdateCumulativeGasWanted( + ctx, + txData.GetGas(), + md.maxGasWanted, + decUtils.GasWanted, + ) + decUtils.GasWanted = gasWanted + + minPriority := evmante.GetMsgPriority( + txData, + decUtils.MinPriority, + decUtils.BaseFee, + ) + decUtils.MinPriority = minPriority + + txFee := evmante.UpdateCumulativeTxFee( + decUtils.TxFee, + txData.Fee(), + decUtils.EvmDenom, + ) + decUtils.TxFee = txFee + decUtils.TxGasLimit += gas + + // 10. increment sequence + acc := md.accountKeeper.GetAccount(ctx, from) + if acc == nil { + // safety check: shouldn't happen + return ctx, errorsmod.Wrapf(errortypes.ErrUnknownAddress, + "account %s does not exist", acc) + } + + if err := evmante.IncrementNonce(ctx, md.accountKeeper, acc, txData.GetNonce()); err != nil { + return ctx, err + } + + // 11. gas wanted + if err := evmante.CheckGasWanted(ctx, md.feeMarketKeeper, tx, decUtils.Rules.IsLondon); err != nil { + return ctx, err + } + + // 12. emit events + txIdx := uint64(i) // nosec: G701 + evmante.EmitTxHashEvent(ctx, ethMsg, decUtils.BlockTxIndex, txIdx) + } + + if err := evmante.CheckTxFee(txFeeInfo, decUtils.TxFee, decUtils.TxGasLimit); err != nil { + return ctx, err + } + + ctx, err = evmante.CheckBlockGasLimit(ctx, decUtils.GasWanted, decUtils.MinPriority) + if err != nil { + return ctx, err + } + + return next(ctx, tx, simulate) +} diff --git a/example_chain/ante/handler_options.go b/example_chain/ante/handler_options.go new file mode 100644 index 0000000..7683a87 --- /dev/null +++ b/example_chain/ante/handler_options.go @@ -0,0 +1,71 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package ante + +import ( + errorsmod "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/codec" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + "github.com/cosmos/cosmos-sdk/x/auth/ante" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + evmtypes "github.com/evmos/evmos/v19/x/evm/types" + evmoscosmosante "github.com/evmos/os/ante/cosmos" +) + +// HandlerOptions defines the list of module keepers required to run the Evmos +// AnteHandler decorators. +type HandlerOptions struct { + Cdc codec.BinaryCodec + AccountKeeper evmtypes.AccountKeeper + BankKeeper evmtypes.BankKeeper + DistributionKeeper evmoscosmosante.DistributionKeeper + StakingKeeper evmoscosmosante.StakingKeeper + FeeMarketKeeper anteinterfaces.FeeMarketKeeper + EvmKeeper anteinterfaces.EVMKeeper + FeegrantKeeper ante.FeegrantKeeper + ExtensionOptionChecker ante.ExtensionOptionChecker + SignModeHandler authsigning.SignModeHandler + SigGasConsumer func(meter storetypes.GasMeter, sig signing.SignatureV2, params authtypes.Params) error + MaxTxGasWanted uint64 + TxFeeChecker ante.TxFeeChecker +} + +// Validate checks if the keepers are defined +func (options HandlerOptions) Validate() error { + if options.Cdc == nil { + return errorsmod.Wrap(errortypes.ErrLogic, "codec is required for AnteHandler") + } + if options.AccountKeeper == nil { + return errorsmod.Wrap(errortypes.ErrLogic, "account keeper is required for AnteHandler") + } + if options.BankKeeper == nil { + return errorsmod.Wrap(errortypes.ErrLogic, "bank keeper is required for AnteHandler") + } + if options.StakingKeeper == nil { + return errorsmod.Wrap(errortypes.ErrLogic, "staking keeper is required for AnteHandler") + } + if options.FeeMarketKeeper == nil { + return errorsmod.Wrap(errortypes.ErrLogic, "fee market keeper is required for AnteHandler") + } + if options.EvmKeeper == nil { + return errorsmod.Wrap(errortypes.ErrLogic, "evm keeper is required for AnteHandler") + } + if options.SigGasConsumer == nil { + return errorsmod.Wrap(errortypes.ErrLogic, "signature gas consumer is required for AnteHandler") + } + if options.SignModeHandler == nil { + return errorsmod.Wrap(errortypes.ErrLogic, "sign mode handler is required for AnteHandler") + } + if options.DistributionKeeper == nil { + return errorsmod.Wrap(errortypes.ErrLogic, "distribution keeper is required for AnteHandler") + } + if options.TxFeeChecker == nil { + return errorsmod.Wrap(errortypes.ErrLogic, "tx fee checker is required for AnteHandler") + } + return nil +} diff --git a/example_chain/app.go b/example_chain/app.go index 5ee1d0b..8e8264b 100644 --- a/example_chain/app.go +++ b/example_chain/app.go @@ -96,6 +96,7 @@ import ( "github.com/evmos/evmos/v19/x/feemarket" feemarketkeeper "github.com/evmos/evmos/v19/x/feemarket/keeper" feemarkettypes "github.com/evmos/evmos/v19/x/feemarket/types" + "github.com/evmos/os/ethereum/eip712" srvflags "github.com/evmos/os/server/flags" evmosutils "github.com/evmos/os/utils" ) @@ -162,6 +163,7 @@ var ( // capabilities aren't needed for testing. type ExampleChain struct { *baseapp.BaseApp + legacyAmino *codec.LegacyAmino appCodec codec.Codec txConfig client.TxConfig @@ -208,11 +210,11 @@ func init() { panic(err) } - DefaultNodeHome = filepath.Join(userHomeDir, ".simapp") + DefaultNodeHome = filepath.Join(userHomeDir, ".osd") } -// NewSimApp returns a reference to an initialized ExampleChain. -func NewSimApp( +// NewExampleApp returns a reference to an initialized ExampleChain. +func NewExampleApp( logger log.Logger, db dbm.DB, traceStore io.Writer, @@ -221,6 +223,7 @@ func NewSimApp( baseAppOptions ...func(*baseapp.BaseApp), ) *ExampleChain { encodingConfig := makeEncodingConfig() + eip712.SetEncodingConfig(encodingConfig) appCodec := encodingConfig.Codec legacyAmino := encodingConfig.Amino diff --git a/example_chain/eth_test_helpers.go b/example_chain/eth_test_helpers.go new file mode 100644 index 0000000..5673837 --- /dev/null +++ b/example_chain/eth_test_helpers.go @@ -0,0 +1,197 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package example_chain + +import ( + "encoding/json" + "time" + + "cosmossdk.io/math" + "cosmossdk.io/simapp" + dbm "github.com/cometbft/cometbft-db" + abci "github.com/cometbft/cometbft/abci/types" + "github.com/cometbft/cometbft/libs/log" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + cmtypes "github.com/cometbft/cometbft/types" + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + "github.com/cosmos/cosmos-sdk/testutil/mock" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/evmos/os/testutil" +) + +// DefaultConsensusParams defines the default Tendermint consensus params used in +// Evmos testing. +var DefaultConsensusParams = &tmproto.ConsensusParams{ + Block: &tmproto.BlockParams{ + MaxBytes: 200000, + MaxGas: -1, // no limit + }, + Evidence: &tmproto.EvidenceParams{ + MaxAgeNumBlocks: 302400, + MaxAgeDuration: 504 * time.Hour, // 3 weeks is the max duration + MaxBytes: 10000, + }, + Validator: &tmproto.ValidatorParams{ + PubKeyTypes: []string{ + cmtypes.ABCIPubKeyTypeEd25519, + }, + }, +} + +// EthDefaultConsensusParams defines the default Tendermint consensus params used in +// EvmosApp testing. +var EthDefaultConsensusParams = &cmtypes.ConsensusParams{ + Block: cmtypes.BlockParams{ + MaxBytes: 200000, + MaxGas: -1, // no limit + }, + Evidence: cmtypes.EvidenceParams{ + MaxAgeNumBlocks: 302400, + MaxAgeDuration: 504 * time.Hour, // 3 weeks is the max duration + MaxBytes: 10000, + }, + Validator: cmtypes.ValidatorParams{ + PubKeyTypes: []string{ + cmtypes.ABCIPubKeyTypeEd25519, + }, + }, +} + +// EthSetup initializes a new EvmosApp. A Nop logger is set in EvmosApp. +func EthSetup(isCheckTx bool, patchGenesis func(*ExampleChain, simapp.GenesisState) simapp.GenesisState) *ExampleChain { + return EthSetupWithDB(isCheckTx, patchGenesis, dbm.NewMemDB()) +} + +// EthSetupWithDB initializes a new ExampleChain. A Nop logger is set in ExampleChain. +func EthSetupWithDB(isCheckTx bool, patchGenesis func(*ExampleChain, simapp.GenesisState) simapp.GenesisState, db dbm.DB) *ExampleChain { + chainID := testutil.ExampleChainID + app := NewExampleApp(log.NewNopLogger(), + db, + nil, + true, + simtestutil.NewAppOptionsWithFlagHome(DefaultNodeHome), + baseapp.SetChainID(chainID), + ) + if !isCheckTx { + // init chain must be called to stop deliverState from being nil + genesisState := NewTestGenesisState(app.AppCodec()) + if patchGenesis != nil { + genesisState = patchGenesis(app, genesisState) + } + + stateBytes, err := json.MarshalIndent(genesisState, "", " ") + if err != nil { + panic(err) + } + + // Initialize the chain + app.InitChain( + abci.RequestInitChain{ + ChainId: chainID, + Validators: []abci.ValidatorUpdate{}, + ConsensusParams: DefaultConsensusParams, + AppStateBytes: stateBytes, + }, + ) + } + + return app +} + +// NewTestGenesisState generate genesis state with single validator +func NewTestGenesisState(codec codec.Codec) simapp.GenesisState { + privVal := mock.NewPV() + pubKey, err := privVal.GetPubKey() + if err != nil { + panic(err) + } + // create validator set with single validator + validator := cmtypes.NewValidator(pubKey, 1) + valSet := cmtypes.NewValidatorSet([]*cmtypes.Validator{validator}) + + // generate genesis account + senderPrivKey := secp256k1.GenPrivKey() + acc := authtypes.NewBaseAccount(senderPrivKey.PubKey().Address().Bytes(), senderPrivKey.PubKey(), 0, 0) + balance := banktypes.Balance{ + Address: acc.GetAddress().String(), + Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(100000000000000))), + } + + genesisState := NewDefaultGenesisState() + return genesisStateWithValSet(codec, genesisState, valSet, []authtypes.GenesisAccount{acc}, balance) +} + +func genesisStateWithValSet(codec codec.Codec, genesisState simapp.GenesisState, + valSet *cmtypes.ValidatorSet, genAccs []authtypes.GenesisAccount, + balances ...banktypes.Balance, +) simapp.GenesisState { + // set genesis accounts + authGenesis := authtypes.NewGenesisState(authtypes.DefaultParams(), genAccs) + genesisState[authtypes.ModuleName] = codec.MustMarshalJSON(authGenesis) + + validators := make([]stakingtypes.Validator, 0, len(valSet.Validators)) + delegations := make([]stakingtypes.Delegation, 0, len(valSet.Validators)) + + bondAmt := sdk.DefaultPowerReduction + + for _, val := range valSet.Validators { + pk, err := cryptocodec.FromTmPubKeyInterface(val.PubKey) + if err != nil { + panic(err) + } + pkAny, err := codectypes.NewAnyWithValue(pk) + if err != nil { + panic(err) + } + validator := stakingtypes.Validator{ + OperatorAddress: sdk.ValAddress(val.Address).String(), + ConsensusPubkey: pkAny, + Jailed: false, + Status: stakingtypes.Bonded, + Tokens: bondAmt, + DelegatorShares: math.LegacyOneDec(), + Description: stakingtypes.Description{}, + UnbondingHeight: int64(0), + UnbondingTime: time.Unix(0, 0).UTC(), + Commission: stakingtypes.NewCommission(math.LegacyZeroDec(), math.LegacyZeroDec(), math.LegacyZeroDec()), + MinSelfDelegation: math.ZeroInt(), + } + validators = append(validators, validator) + delegations = append(delegations, stakingtypes.NewDelegation(genAccs[0].GetAddress(), val.Address.Bytes(), math.LegacyOneDec())) + } + // set validators and delegations + stakingGenesis := stakingtypes.NewGenesisState(stakingtypes.DefaultParams(), validators, delegations) + genesisState[stakingtypes.ModuleName] = codec.MustMarshalJSON(stakingGenesis) + + totalSupply := sdk.NewCoins() + for _, b := range balances { + // add genesis acc tokens to total supply + totalSupply = totalSupply.Add(b.Coins...) + } + + for range delegations { + // add delegated tokens to total supply + totalSupply = totalSupply.Add(sdk.NewCoin(sdk.DefaultBondDenom, bondAmt)) + } + + // add bonded amount to bonded pool module account + balances = append(balances, banktypes.Balance{ + Address: authtypes.NewModuleAddress(stakingtypes.BondedPoolName).String(), + Coins: sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, bondAmt)}, + }) + + // update total supply + bankGenesis := banktypes.NewGenesisState(banktypes.DefaultGenesisState().Params, balances, totalSupply, []banktypes.Metadata{}, []banktypes.SendEnabled{}) + genesisState[banktypes.ModuleName] = codec.MustMarshalJSON(bankGenesis) + + return genesisState +} diff --git a/example_chain/export.go b/example_chain/export.go index d600dcf..673dec2 100644 --- a/example_chain/export.go +++ b/example_chain/export.go @@ -7,13 +7,21 @@ import ( tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + "cosmossdk.io/simapp" servertypes "github.com/cosmos/cosmos-sdk/server/types" sdk "github.com/cosmos/cosmos-sdk/types" slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" "github.com/cosmos/cosmos-sdk/x/staking" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/evmos/os/encoding" ) +// NewDefaultGenesisState generates the default state for the application. +func NewDefaultGenesisState() simapp.GenesisState { + encCfg := encoding.MakeConfig(ModuleBasics) + return ModuleBasics.DefaultGenesis(encCfg.Codec) +} + // ExportAppStateAndValidators exports the state of the application for a genesis // file. func (app *ExampleChain) ExportAppStateAndValidators(forZeroHeight bool, jailAllowedAddrs []string, modulesToExport []string) (servertypes.ExportedApp, error) { diff --git a/example_chain/test_helpers.go b/example_chain/test_helpers.go index 2c43734..e7cf922 100644 --- a/example_chain/test_helpers.go +++ b/example_chain/test_helpers.go @@ -46,7 +46,7 @@ func setup(withGenesis bool, invCheckPeriod uint) (*ExampleChain, GenesisState) appOptions[flags.FlagHome] = DefaultNodeHome appOptions[server.FlagInvCheckPeriod] = invCheckPeriod - app := NewSimApp(log.NewNopLogger(), db, nil, true, appOptions) + app := NewExampleApp(log.NewNopLogger(), db, nil, true, appOptions) if withGenesis { return app, app.DefaultGenesis() } @@ -72,7 +72,7 @@ func NewSimappWithCustomOptions(t *testing.T, isCheckTx bool, options SetupOptio Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100000000000000))), } - app := NewSimApp(options.Logger, options.DB, nil, true, options.AppOpts) + app := NewExampleApp(options.Logger, options.DB, nil, true, options.AppOpts) genesisState := app.DefaultGenesis() genesisState, err = simtestutil.GenesisStateWithValSet(app.AppCodec(), genesisState, valSet, []authtypes.GenesisAccount{acc}, balance) require.NoError(t, err) @@ -223,10 +223,10 @@ func NewTestNetworkFixture() network.TestFixture { } defer os.RemoveAll(dir) - app := NewSimApp(log.NewNopLogger(), dbm.NewMemDB(), nil, true, simtestutil.NewAppOptionsWithFlagHome(dir)) + app := NewExampleApp(log.NewNopLogger(), dbm.NewMemDB(), nil, true, simtestutil.NewAppOptionsWithFlagHome(dir)) appCtr := func(val network.ValidatorI) servertypes.Application { - return NewSimApp( + return NewExampleApp( val.GetCtx().Logger, dbm.NewMemDB(), nil, true, simtestutil.NewAppOptionsWithFlagHome(val.GetCtx().Config.RootDir), bam.SetPruning(pruningtypes.NewPruningOptionsFromString(val.GetAppConfig().Pruning)), diff --git a/example_chain/testutil/abci.go b/example_chain/testutil/abci.go new file mode 100644 index 0000000..1b5f325 --- /dev/null +++ b/example_chain/testutil/abci.go @@ -0,0 +1,260 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package testutil + +import ( + "time" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + abci "github.com/cometbft/cometbft/abci/types" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + tmtypes "github.com/cometbft/cometbft/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/evmos/os/encoding" + app "github.com/evmos/os/example_chain" + "github.com/evmos/os/testutil/tx" +) + +// Commit commits a block at a given time. Reminder: At the end of each +// Tendermint Consensus round the following methods are run +// 1. BeginBlock +// 2. DeliverTx +// 3. EndBlock +// 4. Commit +func Commit(ctx sdk.Context, app *app.ExampleChain, t time.Duration, vs *tmtypes.ValidatorSet) (sdk.Context, error) { + header, err := commit(ctx, app, t, vs) + if err != nil { + return ctx, err + } + + return ctx.WithBlockHeader(header), nil +} + +// CommitAndCreateNewCtx commits a block at a given time creating a ctx with the current settings +// This is useful to keep test settings that could be affected by EndBlockers, e.g. +// setting a baseFee == 0 and expecting this condition to continue after commit +func CommitAndCreateNewCtx(ctx sdk.Context, app *app.ExampleChain, t time.Duration, vs *tmtypes.ValidatorSet) (sdk.Context, error) { + header, err := commit(ctx, app, t, vs) + if err != nil { + return ctx, err + } + + // NewContext function keeps the multistore + // but resets other context fields + // GasMeter is set as InfiniteGasMeter + newCtx := app.BaseApp.NewContext(false, header) + // set the reseted fields to keep the current ctx settings + newCtx = newCtx.WithMinGasPrices(ctx.MinGasPrices()) + newCtx = newCtx.WithEventManager(ctx.EventManager()) + newCtx = newCtx.WithKVGasConfig(ctx.KVGasConfig()) + newCtx = newCtx.WithTransientKVGasConfig(ctx.TransientKVGasConfig()) + + return newCtx, nil +} + +// DeliverTx delivers a cosmos tx for a given set of msgs +func DeliverTx( + ctx sdk.Context, + exampleApp *app.ExampleChain, + priv cryptotypes.PrivKey, + gasPrice *sdkmath.Int, + msgs ...sdk.Msg, +) (abci.ResponseDeliverTx, error) { + txConfig := encoding.MakeConfig(app.ModuleBasics).TxConfig + tx, err := tx.PrepareCosmosTx( + ctx, + exampleApp, + tx.CosmosTxArgs{ + TxCfg: txConfig, + Priv: priv, + ChainID: ctx.ChainID(), + Gas: 10_000_000, + GasPrice: gasPrice, + Msgs: msgs, + }, + ) + if err != nil { + return abci.ResponseDeliverTx{}, err + } + return BroadcastTxBytes(exampleApp, txConfig.TxEncoder(), tx) +} + +// DeliverEthTx generates and broadcasts a Cosmos Tx populated with MsgEthereumTx messages. +// If a private key is provided, it will attempt to sign all messages with the given private key, +// otherwise, it will assume the messages have already been signed. +func DeliverEthTx( + exampleApp *app.ExampleChain, + priv cryptotypes.PrivKey, + msgs ...sdk.Msg, +) (abci.ResponseDeliverTx, error) { + txConfig := encoding.MakeConfig(app.ModuleBasics).TxConfig + + tx, err := tx.PrepareEthTx(txConfig, exampleApp, priv, msgs...) + if err != nil { + return abci.ResponseDeliverTx{}, err + } + res, err := BroadcastTxBytes(exampleApp, txConfig.TxEncoder(), tx) + if err != nil { + return res, err + } + + codec := encoding.MakeConfig(app.ModuleBasics).Codec + if _, err := CheckEthTxResponse(res, codec); err != nil { + return res, err + } + return res, nil +} + +// DeliverEthTxWithoutCheck generates and broadcasts a Cosmos Tx populated with MsgEthereumTx messages. +// If a private key is provided, it will attempt to sign all messages with the given private key, +// otherwise, it will assume the messages have already been signed. It does not check if the Eth tx is +// successful or not. +func DeliverEthTxWithoutCheck( + exampleApp *app.ExampleChain, + priv cryptotypes.PrivKey, + msgs ...sdk.Msg, +) (abci.ResponseDeliverTx, error) { + txConfig := encoding.MakeConfig(app.ModuleBasics).TxConfig + + tx, err := tx.PrepareEthTx(txConfig, exampleApp, priv, msgs...) + if err != nil { + return abci.ResponseDeliverTx{}, err + } + + res, err := BroadcastTxBytes(exampleApp, txConfig.TxEncoder(), tx) + if err != nil { + return abci.ResponseDeliverTx{}, err + } + + return res, nil +} + +// CheckTx checks a cosmos tx for a given set of msgs +func CheckTx( + ctx sdk.Context, + exampleApp *app.ExampleChain, + priv cryptotypes.PrivKey, + gasPrice *sdkmath.Int, + msgs ...sdk.Msg, +) (abci.ResponseCheckTx, error) { + txConfig := encoding.MakeConfig(app.ModuleBasics).TxConfig + + tx, err := tx.PrepareCosmosTx( + ctx, + exampleApp, + tx.CosmosTxArgs{ + TxCfg: txConfig, + Priv: priv, + ChainID: ctx.ChainID(), + GasPrice: gasPrice, + Gas: 10_000_000, + Msgs: msgs, + }, + ) + if err != nil { + return abci.ResponseCheckTx{}, err + } + return checkTxBytes(exampleApp, txConfig.TxEncoder(), tx) +} + +// CheckEthTx checks a Ethereum tx for a given set of msgs +func CheckEthTx( + exampleApp *app.ExampleChain, + priv cryptotypes.PrivKey, + msgs ...sdk.Msg, +) (abci.ResponseCheckTx, error) { + txConfig := encoding.MakeConfig(app.ModuleBasics).TxConfig + + tx, err := tx.PrepareEthTx(txConfig, exampleApp, priv, msgs...) + if err != nil { + return abci.ResponseCheckTx{}, err + } + return checkTxBytes(exampleApp, txConfig.TxEncoder(), tx) +} + +// BroadcastTxBytes encodes a transaction and calls DeliverTx on the app. +func BroadcastTxBytes(app *app.ExampleChain, txEncoder sdk.TxEncoder, tx sdk.Tx) (abci.ResponseDeliverTx, error) { + // bz are bytes to be broadcasted over the network + bz, err := txEncoder(tx) + if err != nil { + return abci.ResponseDeliverTx{}, err + } + + req := abci.RequestDeliverTx{Tx: bz} + res := app.BaseApp.DeliverTx(req) + if res.Code != 0 { + return abci.ResponseDeliverTx{}, errorsmod.Wrapf(errortypes.ErrInvalidRequest, res.Log) + } + + return res, nil +} + +// commit is a private helper function that runs the EndBlocker logic, commits the changes, +// updates the header, runs the BeginBlocker function and returns the updated header +func commit(ctx sdk.Context, app *app.ExampleChain, t time.Duration, vs *tmtypes.ValidatorSet) (tmproto.Header, error) { + header := ctx.BlockHeader() + + if vs != nil { + res := app.EndBlock(abci.RequestEndBlock{Height: header.Height}) + + nextVals, err := applyValSetChanges(vs, res.ValidatorUpdates) + if err != nil { + return header, err + } + header.ValidatorsHash = vs.Hash() + header.NextValidatorsHash = nextVals.Hash() + } else { + app.EndBlocker(ctx, abci.RequestEndBlock{Height: header.Height}) + } + + _ = app.Commit() + + header.Height++ + header.Time = header.Time.Add(t) + header.AppHash = app.LastCommitID().Hash + + app.BeginBlock(abci.RequestBeginBlock{ + Header: header, + }) + + return header, nil +} + +// checkTxBytes encodes a transaction and calls checkTx on the app. +func checkTxBytes(app *app.ExampleChain, txEncoder sdk.TxEncoder, tx sdk.Tx) (abci.ResponseCheckTx, error) { + bz, err := txEncoder(tx) + if err != nil { + return abci.ResponseCheckTx{}, err + } + + req := abci.RequestCheckTx{Tx: bz} + res := app.BaseApp.CheckTx(req) + if res.Code != 0 { + return abci.ResponseCheckTx{}, errorsmod.Wrapf(errortypes.ErrInvalidRequest, res.Log) + } + + return res, nil +} + +// applyValSetChanges takes in tmtypes.ValidatorSet and []abci.ValidatorUpdate and will return a new tmtypes.ValidatorSet which has the +// provided validator updates applied to the provided validator set. +func applyValSetChanges(valSet *tmtypes.ValidatorSet, valUpdates []abci.ValidatorUpdate) (*tmtypes.ValidatorSet, error) { + updates, err := tmtypes.PB2TM.ValidatorUpdates(valUpdates) + if err != nil { + return nil, err + } + + // must copy since validator set will mutate with UpdateWithChangeSet + newVals := valSet.Copy() + err = newVals.UpdateWithChangeSet(updates) + if err != nil { + return nil, err + } + + return newVals, nil +} diff --git a/example_chain/testutil/contract.go b/example_chain/testutil/contract.go new file mode 100644 index 0000000..82084c3 --- /dev/null +++ b/example_chain/testutil/contract.go @@ -0,0 +1,164 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package testutil + +import ( + "fmt" + "math/big" + + "github.com/cosmos/gogoproto/proto" + + abci "github.com/cometbft/cometbft/abci/types" + "github.com/cosmos/cosmos-sdk/codec" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + + evm "github.com/evmos/evmos/v19/x/evm/types" + app "github.com/evmos/os/example_chain" + "github.com/evmos/os/testutil/tx" +) + +// ContractArgs are the params used for calling a smart contract. +type ContractArgs struct { + // Addr is the address of the contract to call. + Addr common.Address + // ABI is the ABI of the contract to call. + ABI abi.ABI + // MethodName is the name of the method to call. + MethodName string + // Args are the arguments to pass to the method. + Args []interface{} +} + +// ContractCallArgs is the arguments for calling a smart contract. +type ContractCallArgs struct { + // Contract are the contract-specific arguments required for the contract call. + Contract ContractArgs + // Nonce is the nonce to use for the transaction. + Nonce *big.Int + // Amount is the aevmos amount to send in the transaction. + Amount *big.Int + // GasLimit to use for the transaction + GasLimit uint64 + // PrivKey is the private key to be used for the transaction. + PrivKey cryptotypes.PrivKey +} + +// DeployContract deploys a contract with the provided private key, +// compiled contract data and constructor arguments +func DeployContract( + ctx sdk.Context, + app *app.ExampleChain, + priv cryptotypes.PrivKey, + queryClientEvm evm.QueryClient, + contract evm.CompiledContract, + constructorArgs ...interface{}, +) (common.Address, error) { + chainID := app.EVMKeeper.ChainID() + from := common.BytesToAddress(priv.PubKey().Address().Bytes()) + nonce := app.EVMKeeper.GetNonce(ctx, from) + + ctorArgs, err := contract.ABI.Pack("", constructorArgs...) + if err != nil { + return common.Address{}, err + } + + data := append(contract.Bin, ctorArgs...) //nolint:gocritic + gas, err := tx.GasLimit(ctx, from, data, queryClientEvm) + if err != nil { + return common.Address{}, err + } + + msgEthereumTx := evm.NewTx(&evm.EvmTxArgs{ + ChainID: chainID, + Nonce: nonce, + GasLimit: gas, + GasFeeCap: app.FeeMarketKeeper.GetBaseFee(ctx), + GasTipCap: big.NewInt(1), + Input: data, + Accesses: ðtypes.AccessList{}, + }) + msgEthereumTx.From = from.String() + + res, err := DeliverEthTx(app, priv, msgEthereumTx) + if err != nil { + return common.Address{}, err + } + + if _, err := CheckEthTxResponse(res, app.AppCodec()); err != nil { + return common.Address{}, err + } + + return crypto.CreateAddress(from, nonce), nil +} + +// DeployContractWithFactory deploys a contract using a contract factory +// with the provided factoryAddress +func DeployContractWithFactory( + ctx sdk.Context, + evmosApp *app.ExampleChain, + priv cryptotypes.PrivKey, + factoryAddress common.Address, +) (common.Address, abci.ResponseDeliverTx, error) { + chainID := evmosApp.EVMKeeper.ChainID() + from := common.BytesToAddress(priv.PubKey().Address().Bytes()) + factoryNonce := evmosApp.EVMKeeper.GetNonce(ctx, factoryAddress) + nonce := evmosApp.EVMKeeper.GetNonce(ctx, from) + + msgEthereumTx := evm.NewTx(&evm.EvmTxArgs{ + ChainID: chainID, + Nonce: nonce, + To: &factoryAddress, + GasLimit: uint64(100000), + GasPrice: big.NewInt(1000000000), + }) + msgEthereumTx.From = from.String() + + res, err := DeliverEthTx(evmosApp, priv, msgEthereumTx) + if err != nil { + return common.Address{}, abci.ResponseDeliverTx{}, err + } + + if _, err := CheckEthTxResponse(res, evmosApp.AppCodec()); err != nil { + return common.Address{}, abci.ResponseDeliverTx{}, err + } + + return crypto.CreateAddress(factoryAddress, factoryNonce), res, err +} + +// CheckEthTxResponse checks that the transaction was executed successfully +func CheckEthTxResponse(r abci.ResponseDeliverTx, cdc codec.Codec) ([]*evm.MsgEthereumTxResponse, error) { + if !r.IsOK() { + return nil, fmt.Errorf("tx failed. Code: %d, Logs: %s", r.Code, r.Log) + } + + var txData sdk.TxMsgData + if err := cdc.Unmarshal(r.Data, &txData); err != nil { + return nil, err + } + + if len(txData.MsgResponses) == 0 { + return nil, fmt.Errorf("no message responses found") + } + + responses := make([]*evm.MsgEthereumTxResponse, 0, len(txData.MsgResponses)) + for i := range txData.MsgResponses { + var res evm.MsgEthereumTxResponse + if err := proto.Unmarshal(txData.MsgResponses[i].Value, &res); err != nil { + return nil, err + } + + if res.Failed() { + return nil, fmt.Errorf("tx failed. VmError: %s", res.VmError) + } + responses = append(responses, &res) + } + + return responses, nil +} diff --git a/example_chain/testutil/fund.go b/example_chain/testutil/fund.go new file mode 100644 index 0000000..27ca65f --- /dev/null +++ b/example_chain/testutil/fund.go @@ -0,0 +1,41 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package testutil + +import ( + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + "github.com/evmos/evmos/os/utils" +) + +// FundAccount is a utility function that funds an account by minting and +// sending the coins to the address. +func FundAccount(ctx sdk.Context, bankKeeper bankkeeper.Keeper, addr sdk.AccAddress, amounts sdk.Coins) error { + if err := bankKeeper.MintCoins(ctx, minttypes.ModuleName, amounts); err != nil { + return err + } + + return bankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, addr, amounts) +} + +// FundAccountWithBaseDenom is a utility function that uses the FundAccount function +// to fund an account with the default Evmos denomination. +func FundAccountWithBaseDenom(ctx sdk.Context, bankKeeper bankkeeper.Keeper, addr sdk.AccAddress, amount int64) error { + coins := sdk.NewCoins( + sdk.NewCoin(utils.BaseDenom, math.NewInt(amount)), + ) + return FundAccount(ctx, bankKeeper, addr, coins) +} + +// FundModuleAccount is a utility function that funds a module account by +// minting and sending the coins to the address. +func FundModuleAccount(ctx sdk.Context, bankKeeper bankkeeper.Keeper, recipientMod string, amounts sdk.Coins) error { + if err := bankKeeper.MintCoins(ctx, minttypes.ModuleName, amounts); err != nil { + return err + } + + return bankKeeper.SendCoinsFromModuleToModule(ctx, minttypes.ModuleName, recipientMod, amounts) +} diff --git a/testutil/ante.go b/testutil/ante.go new file mode 100644 index 0000000..06cdcfc --- /dev/null +++ b/testutil/ante.go @@ -0,0 +1,17 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package testutil + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// NoOpNextFn is a no-op function that returns the context and no error in order to mock +// the next function in the AnteHandler chain. +// +// It can be used in unit tests when calling a decorator's AnteHandle method, e.g. +// `dec.AnteHandle(ctx, tx, false, NoOpNextFn)` +func NoOpNextFn(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { + return ctx, nil +} diff --git a/testutil/tx/cosmos.go b/testutil/tx/cosmos.go index e714d1e..68921d1 100644 --- a/testutil/tx/cosmos.go +++ b/testutil/tx/cosmos.go @@ -1,4 +1,4 @@ -// Copyright Tharsis Labs Ltd.(Evmos) +// Copyright Tharsis Labs Ltd.(ExampleChain) // SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) package tx @@ -11,7 +11,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/tx/signing" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" - "github.com/evmos/evmos/v19/app" + app "github.com/evmos/os/example_chain" "github.com/evmos/os/testutil" ) @@ -41,7 +41,7 @@ type CosmosTxArgs struct { // It returns the signed transaction and an error func PrepareCosmosTx( ctx sdk.Context, - appEvmos *app.Evmos, + app *app.ExampleChain, args CosmosTxArgs, ) (authsigning.Tx, error) { txBuilder := args.TxCfg.NewTxBuilder() @@ -64,7 +64,7 @@ func PrepareCosmosTx( return signCosmosTx( ctx, - appEvmos, + app, args, txBuilder, ) @@ -74,12 +74,12 @@ func PrepareCosmosTx( // the provided private key func signCosmosTx( ctx sdk.Context, - appEvmos *app.Evmos, + app *app.ExampleChain, args CosmosTxArgs, txBuilder client.TxBuilder, ) (authsigning.Tx, error) { addr := sdk.AccAddress(args.Priv.PubKey().Address().Bytes()) - seq, err := appEvmos.AccountKeeper.GetSequence(ctx, addr) + seq, err := app.AccountKeeper.GetSequence(ctx, addr) if err != nil { return nil, err } @@ -102,7 +102,7 @@ func signCosmosTx( } // Second round: all signer infos are set, so each signer can sign. - accNumber := appEvmos.AccountKeeper.GetAccount(ctx, addr).GetAccountNumber() + accNumber := app.AccountKeeper.GetAccount(ctx, addr).GetAccountNumber() signerData := authsigning.SignerData{ ChainID: args.ChainID, AccountNumber: accNumber, diff --git a/testutil/tx/eip712.go b/testutil/tx/eip712.go index 1c461c7..edc6798 100644 --- a/testutil/tx/eip712.go +++ b/testutil/tx/eip712.go @@ -15,9 +15,9 @@ import ( authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/signer/core/apitypes" - "github.com/evmos/evmos/v19/app" cryptocodec "github.com/evmos/os/crypto/codec" "github.com/evmos/os/ethereum/eip712" + app "github.com/evmos/os/example_chain" "github.com/evmos/os/types" ) @@ -44,12 +44,12 @@ type signatureV2Args struct { // It returns the signed transaction and an error func CreateEIP712CosmosTx( ctx sdk.Context, - appEvmos *app.Evmos, + app *app.ExampleChain, args EIP712TxArgs, ) (sdk.Tx, error) { builder, err := PrepareEIP712CosmosTx( ctx, - appEvmos, + app, args, ) return builder.GetTx(), err @@ -60,7 +60,7 @@ func CreateEIP712CosmosTx( // It returns the tx builder with the signed transaction and an error func PrepareEIP712CosmosTx( ctx sdk.Context, - appEvmos *app.Evmos, + exampleApp *app.ExampleChain, args EIP712TxArgs, ) (client.TxBuilder, error) { txArgs := args.CosmosTxArgs @@ -72,9 +72,9 @@ func PrepareEIP712CosmosTx( chainIDNum := pc.Uint64() from := sdk.AccAddress(txArgs.Priv.PubKey().Address().Bytes()) - accNumber := appEvmos.AccountKeeper.GetAccount(ctx, from).GetAccountNumber() + accNumber := exampleApp.AccountKeeper.GetAccount(ctx, from).GetAccountNumber() - nonce, err := appEvmos.AccountKeeper.GetSequence(ctx, from) + nonce, err := exampleApp.AccountKeeper.GetSequence(ctx, from) if err != nil { return nil, err } @@ -112,7 +112,7 @@ func PrepareEIP712CosmosTx( return signCosmosEIP712Tx( ctx, - appEvmos, + exampleApp, args, builder, typedData, @@ -123,7 +123,7 @@ func PrepareEIP712CosmosTx( // the provided private key and the typed data func signCosmosEIP712Tx( ctx sdk.Context, - appEvmos *app.Evmos, + exampleApp *app.ExampleChain, args EIP712TxArgs, builder authtx.ExtensionOptionsTxBuilder, data apitypes.TypedData, @@ -131,7 +131,7 @@ func signCosmosEIP712Tx( priv := args.CosmosTxArgs.Priv from := sdk.AccAddress(priv.PubKey().Address().Bytes()) - nonce, err := appEvmos.AccountKeeper.GetSequence(ctx, from) + nonce, err := exampleApp.AccountKeeper.GetSequence(ctx, from) if err != nil { return nil, err } diff --git a/testutil/tx/eth.go b/testutil/tx/eth.go index d350c17..149ad35 100644 --- a/testutil/tx/eth.go +++ b/testutil/tx/eth.go @@ -18,8 +18,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/evmos/evmos/v19/app" evmtypes "github.com/evmos/evmos/v19/x/evm/types" + app "github.com/evmos/os/example_chain" "github.com/evmos/os/server/config" "github.com/evmos/os/testutil" ) @@ -28,13 +28,13 @@ import ( // It returns the signed transaction and an error func PrepareEthTx( txCfg client.TxConfig, - appEvmos *app.Evmos, + app *app.ExampleChain, priv cryptotypes.PrivKey, msgs ...sdk.Msg, ) (authsigning.Tx, error) { txBuilder := txCfg.NewTxBuilder() - signer := ethtypes.LatestSignerForChainID(appEvmos.EvmKeeper.ChainID()) + signer := ethtypes.LatestSignerForChainID(app.EVMKeeper.ChainID()) txFee := sdk.Coins{} txGasLimit := uint64(0) @@ -91,7 +91,7 @@ func PrepareEthTx( // Should this not be the case, just pass in zero. func CreateEthTx( ctx sdk.Context, - appEvmos *app.Evmos, + app *app.ExampleChain, privKey cryptotypes.PrivKey, from sdk.AccAddress, dest sdk.AccAddress, @@ -100,17 +100,17 @@ func CreateEthTx( ) (*evmtypes.MsgEthereumTx, error) { toAddr := common.BytesToAddress(dest.Bytes()) fromAddr := common.BytesToAddress(from.Bytes()) - chainID := appEvmos.EvmKeeper.ChainID() + chainID := app.EVMKeeper.ChainID() // When we send multiple Ethereum Tx's in one Cosmos Tx, we need to increment the nonce for each one. - nonce := appEvmos.EvmKeeper.GetNonce(ctx, fromAddr) + uint64(nonceIncrement) + nonce := app.EVMKeeper.GetNonce(ctx, fromAddr) + uint64(nonceIncrement) evmTxParams := &evmtypes.EvmTxArgs{ ChainID: chainID, Nonce: nonce, To: &toAddr, Amount: amount, GasLimit: 100000, - GasFeeCap: appEvmos.FeeMarketKeeper.GetBaseFee(ctx), + GasFeeCap: app.FeeMarketKeeper.GetBaseFee(ctx), GasTipCap: big.NewInt(1), Accesses: ðtypes.AccessList{}, } @@ -119,7 +119,7 @@ func CreateEthTx( // If we are creating multiple eth Tx's with different senders, we need to sign here rather than later. if privKey != nil { - signer := ethtypes.LatestSignerForChainID(appEvmos.EvmKeeper.ChainID()) + signer := ethtypes.LatestSignerForChainID(app.EVMKeeper.ChainID()) err := msgEthereumTx.Sign(signer, NewSigner(privKey)) if err != nil { return nil, err