From c82c66815e1299ee191d808629215dabdd95486d Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Wed, 5 Jun 2024 21:14:36 +0400 Subject: [PATCH 01/13] test(evm): unit tests for evm_ante_sigverify --- app/evmante_sigverify.go | 9 +++- app/evmante_sigverify_test.go | 88 +++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 app/evmante_sigverify_test.go diff --git a/app/evmante_sigverify.go b/app/evmante_sigverify.go index 25d0fb2bc..a25b93d29 100644 --- a/app/evmante_sigverify.go +++ b/app/evmante_sigverify.go @@ -42,7 +42,10 @@ func (esvd EthSigVerificationDecorator) AnteHandle( for _, msg := range tx.GetMsgs() { msgEthTx, ok := msg.(*evm.MsgEthereumTx) if !ok { - return ctx, errors.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + return ctx, errors.Wrapf( + errortypes.ErrUnknownRequest, + "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil), + ) } allowUnprotectedTxs := evmParams.GetAllowUnprotectedTxs() @@ -50,7 +53,9 @@ func (esvd EthSigVerificationDecorator) AnteHandle( if !allowUnprotectedTxs && !ethTx.Protected() { return ctx, errors.Wrapf( errortypes.ErrNotSupported, - "rejected unprotected Ethereum transaction. Please EIP155 sign your transaction to protect it against replay-attacks") + "rejected unprotected Ethereum transaction. "+ + "Please EIP155 sign your transaction to protect it against replay-attacks", + ) } sender, err := signer.Sender(ethTx) diff --git a/app/evmante_sigverify_test.go b/app/evmante_sigverify_test.go new file mode 100644 index 000000000..2b70e5eac --- /dev/null +++ b/app/evmante_sigverify_test.go @@ -0,0 +1,88 @@ +package app_test + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/x/evm/evmtest" + tf "github.com/NibiruChain/nibiru/x/tokenfactory/types" +) + +var InvalidChainID = big.NewInt(987654321) +var RandomAddress = evmtest.NewEthAccInfo().EthAddr.Hex() + +func (s *TestSuite) TestEthSigVerificationDecorator() { + testCases := []struct { + name string + txSetup func(deps *evmtest.TestDeps) sdk.Tx + wantErr string + }{ + { + name: "sad: unsigned tx", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + tx := happyCreateContractTx(deps) + return tx + }, + wantErr: "rejected unprotected Ethereum transaction", + }, + { + name: "sad: non ethereum tx", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return legacytx.StdTx{ + Msgs: []sdk.Msg{ + &tf.MsgMint{}, + }, + } + }, + wantErr: "invalid message", + }, + { + name: "sad: ethereum tx invalid chain id", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + tx := happyCreateContractTx(deps) + gethSigner := deps.Sender.GethSigner(InvalidChainID) + keyringSigner := deps.Sender.KeyringSigner + err := tx.Sign(gethSigner, keyringSigner) + s.Require().NoError(err) + return tx + }, + wantErr: "invalid chain id for signer", + }, + { + name: "happy: signed ethereum tx", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + tx := happyCreateContractTx(deps) + gethSigner := deps.Sender.GethSigner(deps.Chain.EvmKeeper.EthChainID(deps.Ctx)) + keyringSigner := deps.Sender.KeyringSigner + err := tx.Sign(gethSigner, keyringSigner) + s.Require().NoError(err) + return tx + }, + wantErr: "", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + stateDB := deps.StateDB() + anteDec := app.NewEthSigVerificationDecorator(deps.Chain.AppKeepers) + + tx := tc.txSetup(&deps) + s.Require().NoError(stateDB.Commit()) + + deps.Ctx = deps.Ctx.WithIsCheckTx(true) + _, err := anteDec.AnteHandle( + deps.Ctx, tx, false, NextNoOpAnteHandler, + ) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + }) + } +} From cba648fabe6b121fd031505a6593caac8cc46544 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Wed, 5 Jun 2024 21:17:23 +0400 Subject: [PATCH 02/13] chore: changelog update --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a1ae768b..545338913 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#1895](https://github.com/NibiruChain/nibiru/pull/1895) - refactor(geth): Reference go-ethereum as a submodule for easier change tracking with upstream - [#1901](https://github.com/NibiruChain/nibiru/pull/1901) - test(evm): more e2e test contracts for edge cases - [#1909](https://github.com/NibiruChain/nibiru/pull/1909) - chore(evm): set is_london true by default and removed from config +- [#1912](https://github.com/NibiruChain/nibiru/pull/1912) - test(evm): unit tests for evm_ante_sigverify #### Dapp modules: perp, spot, oracle, etc From af87b510becaacab0d9a76a5fd63bff3e4b17de0 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Fri, 7 Jun 2024 11:48:08 +0400 Subject: [PATCH 03/13] test(evm): ante gas wanted full coverage --- ...te_fee_market.go => evmante_gas_wanted.go} | 0 app/evmante_gas_wanted_test.go | 106 ++++++++++++++++++ 2 files changed, 106 insertions(+) rename app/{evmante_fee_market.go => evmante_gas_wanted.go} (100%) create mode 100644 app/evmante_gas_wanted_test.go diff --git a/app/evmante_fee_market.go b/app/evmante_gas_wanted.go similarity index 100% rename from app/evmante_fee_market.go rename to app/evmante_gas_wanted.go diff --git a/app/evmante_gas_wanted_test.go b/app/evmante_gas_wanted_test.go new file mode 100644 index 000000000..28ba85ce6 --- /dev/null +++ b/app/evmante_gas_wanted_test.go @@ -0,0 +1,106 @@ +package app_test + +import ( + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +func (s *TestSuite) TestGasWantedDecorator() { + testCases := []struct { + name string + ctxSetup func(deps *evmtest.TestDeps) + txSetup func(deps *evmtest.TestDeps) sdk.Tx + wantErr string + }{ + { + name: "happy: non fee tx type", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return happyCreateContractTx(deps) + }, + wantErr: "", + }, + { + name: "happy: tx without gas, block gas limit 1000", + ctxSetup: func(deps *evmtest.TestDeps) { + cp := &tmproto.ConsensusParams{ + Block: &tmproto.BlockParams{MaxGas: 1000}, + } + deps.Ctx = deps.Ctx.WithConsensusParams(cp) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return legacytx.StdTx{ + Msgs: []sdk.Msg{ + happyCreateContractTx(deps), + }, + } + }, + wantErr: "", + }, + { + name: "happy: tx with gas wanted 500, block gas limit 1000", + ctxSetup: func(deps *evmtest.TestDeps) { + cp := &tmproto.ConsensusParams{ + Block: &tmproto.BlockParams{MaxGas: 1000}, + } + deps.Ctx = deps.Ctx.WithConsensusParams(cp) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return legacytx.StdTx{ + Msgs: []sdk.Msg{ + happyCreateContractTx(deps), + }, + Fee: legacytx.StdFee{Gas: 500}, + } + }, + wantErr: "", + }, + { + name: "sad: tx with gas wanted 1000, block gas limit 500", + ctxSetup: func(deps *evmtest.TestDeps) { + cp := &tmproto.ConsensusParams{ + Block: &tmproto.BlockParams{ + MaxGas: 500, + }, + } + deps.Ctx = deps.Ctx.WithConsensusParams(cp) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return legacytx.StdTx{ + Msgs: []sdk.Msg{ + happyCreateContractTx(deps), + }, + Fee: legacytx.StdFee{Gas: 1000}, + } + }, + wantErr: "exceeds block gas limit", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + stateDB := deps.StateDB() + anteDec := app.NewGasWantedDecorator(deps.Chain.AppKeepers) + + tx := tc.txSetup(&deps) + s.Require().NoError(stateDB.Commit()) + + deps.Ctx = deps.Ctx.WithIsCheckTx(true) + if tc.ctxSetup != nil { + tc.ctxSetup(&deps) + } + _, err := anteDec.AnteHandle( + deps.Ctx, tx, false, NextNoOpAnteHandler, + ) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + }) + } +} From a862309f79b28965ec4b76a5f767222c8f1cc5c1 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Fri, 7 Jun 2024 18:09:49 +0400 Subject: [PATCH 04/13] test(evm): ante handler setup ctx coverage --- app/evmante_emit_event.go | 49 +++++++++++ app/evmante_setup_ctx.go | 159 +--------------------------------- app/evmante_setup_ctx_test.go | 42 +++++++++ app/evmante_validate_basic.go | 123 ++++++++++++++++++++++++++ 4 files changed, 218 insertions(+), 155 deletions(-) create mode 100644 app/evmante_emit_event.go create mode 100644 app/evmante_setup_ctx_test.go create mode 100644 app/evmante_validate_basic.go diff --git a/app/evmante_emit_event.go b/app/evmante_emit_event.go new file mode 100644 index 000000000..81c24fcb2 --- /dev/null +++ b/app/evmante_emit_event.go @@ -0,0 +1,49 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package app + +import ( + "strconv" + + errorsmod "cosmossdk.io/errors" + "github.com/NibiruChain/nibiru/x/evm" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" +) + +// EthEmitEventDecorator emit events in ante handler in case of tx execution failed (out of block gas limit). +type EthEmitEventDecorator struct { + AppKeepers +} + +// NewEthEmitEventDecorator creates a new EthEmitEventDecorator +func NewEthEmitEventDecorator(k AppKeepers) EthEmitEventDecorator { + return EthEmitEventDecorator{AppKeepers: k} +} + +// 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 events at the + // very end of ante handler to be indexed by the consensus engine. + txIndex := eeed.EvmKeeper.EVMState().BlockTxIndex.GetOr(ctx, 0) + + for i, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evm.MsgEthereumTx) + if !ok { + return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + } + + // 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( + evm.EventTypeEthereumTx, + sdk.NewAttribute(evm.AttributeKeyEthereumTxHash, msgEthTx.Hash), + sdk.NewAttribute(evm.AttributeKeyTxIndex, strconv.FormatUint(txIndex+uint64(i), 10)), // #nosec G701 + )) + } + + return next(ctx, tx, simulate) +} diff --git a/app/evmante_setup_ctx.go b/app/evmante_setup_ctx.go index 847bd77bd..be4d5f897 100644 --- a/app/evmante_setup_ctx.go +++ b/app/evmante_setup_ctx.go @@ -2,18 +2,11 @@ package app import ( - "errors" - "strconv" - errorsmod "cosmossdk.io/errors" - sdkmath "cosmossdk.io/math" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" errortypes "github.com/cosmos/cosmos-sdk/types/errors" authante "github.com/cosmos/cosmos-sdk/x/auth/ante" - gethcore "github.com/ethereum/go-ethereum/core/types" - - "github.com/NibiruChain/nibiru/x/evm" ) // EthSetupContextDecorator is adapted from SetUpContextDecorator from cosmos-sdk, it ignores gas consumption @@ -37,7 +30,10 @@ func (esc EthSetupContextDecorator) AnteHandle( // all transactions must implement GasTx _, ok := tx.(authante.GasTx) if !ok { - return ctx, errorsmod.Wrapf(errortypes.ErrInvalidType, "invalid transaction type %T, expected GasTx", tx) + return ctx, errorsmod.Wrapf( + errortypes.ErrInvalidType, + "invalid transaction type %T, expected GasTx", tx, + ) } // We need to setup an empty gas config so that the gas is consistent with Ethereum. @@ -50,150 +46,3 @@ func (esc EthSetupContextDecorator) AnteHandle( esc.EvmKeeper.ResetTransientGasUsed(ctx) return next(newCtx, tx, simulate) } - -// EthEmitEventDecorator emit events in ante handler in case of tx execution failed (out of block gas limit). -type EthEmitEventDecorator struct { - AppKeepers -} - -// NewEthEmitEventDecorator creates a new EthEmitEventDecorator -func NewEthEmitEventDecorator(k AppKeepers) EthEmitEventDecorator { - return EthEmitEventDecorator{AppKeepers: k} -} - -// 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 events at the - // very end of ante handler to be indexed by the consensus engine. - txIndex := eeed.EvmKeeper.EVMState().BlockTxIndex.GetOr(ctx, 0) - - for i, msg := range tx.GetMsgs() { - msgEthTx, ok := msg.(*evm.MsgEthereumTx) - if !ok { - return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) - } - - // 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( - evm.EventTypeEthereumTx, - sdk.NewAttribute(evm.AttributeKeyEthereumTxHash, msgEthTx.Hash), - sdk.NewAttribute(evm.AttributeKeyTxIndex, strconv.FormatUint(txIndex+uint64(i), 10)), // #nosec G701 - )) - } - - return next(ctx, tx, simulate) -} - -// EthValidateBasicDecorator is adapted from ValidateBasicDecorator from cosmos-sdk, it ignores ErrNoSignatures -type EthValidateBasicDecorator struct { - AppKeepers -} - -// NewEthValidateBasicDecorator creates a new EthValidateBasicDecorator -func NewEthValidateBasicDecorator(k AppKeepers) EthValidateBasicDecorator { - return EthValidateBasicDecorator{ - AppKeepers: k, - } -} - -// AnteHandle handles basic validation of tx -func (vbd EthValidateBasicDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { - // no need to validate basic on recheck tx, call next antehandler - if ctx.IsReCheckTx() { - return next(ctx, tx, simulate) - } - - err := tx.ValidateBasic() - // ErrNoSignatures is fine with eth tx - if err != nil && !errors.Is(err, errortypes.ErrNoSignatures) { - return ctx, 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.(protoTxProvider) - if !ok { - return ctx, 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 ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, - "for eth tx body Memo TimeoutHeight NonCriticalExtensionOptions should be empty") - } - - if len(body.ExtensionOptions) != 1 { - return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx length of ExtensionOptions should be 1") - } - - authInfo := protoTx.AuthInfo - if len(authInfo.SignerInfos) > 0 { - return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx AuthInfo SignerInfos should be empty") - } - - if authInfo.Fee.Payer != "" || authInfo.Fee.Granter != "" { - return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx AuthInfo Fee payer and granter should be empty") - } - - sigs := protoTx.Signatures - if len(sigs) > 0 { - return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx Signatures should be empty") - } - - txFee := sdk.Coins{} - txGasLimit := uint64(0) - - evmParams := vbd.EvmKeeper.GetParams(ctx) - baseFee := vbd.EvmKeeper.GetBaseFee(ctx) - enableCreate := evmParams.GetEnableCreate() - enableCall := evmParams.GetEnableCall() - evmDenom := evmParams.GetEvmDenom() - - for _, msg := range protoTx.GetMsgs() { - msgEthTx, ok := msg.(*evm.MsgEthereumTx) - if !ok { - return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) - } - - // Validate `From` field - if msgEthTx.From != "" { - return ctx, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid From %s, expect empty string", msgEthTx.From) - } - - txGasLimit += msgEthTx.GetGas() - - txData, err := evm.UnpackTxData(msgEthTx.Data) - if err != nil { - return ctx, errorsmod.Wrap(err, "failed to unpack MsgEthereumTx Data") - } - - // return error if contract creation or call are disabled through governance - if !enableCreate && txData.GetTo() == nil { - return ctx, errorsmod.Wrap(evm.ErrCreateDisabled, "failed to create new contract") - } else if !enableCall && txData.GetTo() != nil { - return ctx, errorsmod.Wrap(evm.ErrCallDisabled, "failed to call contract") - } - - if baseFee == nil && txData.TxType() == gethcore.DynamicFeeTxType { - return ctx, errorsmod.Wrap(gethcore.ErrTxTypeNotSupported, "dynamic fee tx not supported") - } - - txFee = txFee.Add(sdk.Coin{Denom: evmDenom, Amount: sdkmath.NewIntFromBigInt(txData.Fee())}) - } - - if !authInfo.Fee.Amount.IsEqual(txFee) { - return ctx, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid AuthInfo Fee Amount (%s != %s)", authInfo.Fee.Amount, txFee) - } - - if authInfo.Fee.GasLimit != txGasLimit { - return ctx, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid AuthInfo Fee GasLimit (%d != %d)", authInfo.Fee.GasLimit, txGasLimit) - } - - return next(ctx, tx, simulate) -} diff --git a/app/evmante_setup_ctx_test.go b/app/evmante_setup_ctx_test.go new file mode 100644 index 000000000..5a3597414 --- /dev/null +++ b/app/evmante_setup_ctx_test.go @@ -0,0 +1,42 @@ +package app_test + +import ( + "math" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/x/evm/evmtest" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func (s *TestSuite) TestEthSetupContextDecorator() { + deps := evmtest.NewTestDeps() + stateDB := deps.StateDB() + anteDec := app.NewEthSetUpContextDecorator(deps.Chain.AppKeepers) + + s.Require().NoError(stateDB.Commit()) + tx := happyCreateContractTx(&deps) + + // Set block gas used to non 0 to check that handler resets it + anteDec.EvmKeeper.EvmState.BlockGasUsed.Set(deps.Ctx, 1000) + + // Ante handler returns new context + newCtx, err := anteDec.AnteHandle( + deps.Ctx, tx, false, NextNoOpAnteHandler, + ) + s.Require().NoError(err) + + // Check that ctx gas meter is set up to infinite + ctxGasMeter := newCtx.GasMeter() + s.Require().Equal(sdk.Gas(math.MaxUint64), ctxGasMeter.GasRemaining()) + + // Check that gas configs are reset to default + defaultGasConfig := storetypes.GasConfig{} + s.Require().Equal(defaultGasConfig, newCtx.KVGasConfig()) + s.Require().Equal(defaultGasConfig, newCtx.TransientKVGasConfig()) + + // Check that block gas used is reset to 0 + gas, err := anteDec.EvmKeeper.EvmState.BlockGasUsed.Get(newCtx) + s.Require().NoError(err) + s.Require().Equal(gas, uint64(0)) +} diff --git a/app/evmante_validate_basic.go b/app/evmante_validate_basic.go new file mode 100644 index 000000000..292edf3d9 --- /dev/null +++ b/app/evmante_validate_basic.go @@ -0,0 +1,123 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package app + +import ( + "errors" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + gethcore "github.com/ethereum/go-ethereum/core/types" + + "github.com/NibiruChain/nibiru/x/evm" +) + +// EthValidateBasicDecorator is adapted from ValidateBasicDecorator from cosmos-sdk, it ignores ErrNoSignatures +type EthValidateBasicDecorator struct { + AppKeepers +} + +// NewEthValidateBasicDecorator creates a new EthValidateBasicDecorator +func NewEthValidateBasicDecorator(k AppKeepers) EthValidateBasicDecorator { + return EthValidateBasicDecorator{ + AppKeepers: k, + } +} + +// AnteHandle handles basic validation of tx +func (vbd EthValidateBasicDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + // no need to validate basic on recheck tx, call next antehandler + if ctx.IsReCheckTx() { + return next(ctx, tx, simulate) + } + + err := tx.ValidateBasic() + // ErrNoSignatures is fine with eth tx + if err != nil && !errors.Is(err, errortypes.ErrNoSignatures) { + return ctx, 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.(protoTxProvider) + if !ok { + return ctx, 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 ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, + "for eth tx body Memo TimeoutHeight NonCriticalExtensionOptions should be empty") + } + + if len(body.ExtensionOptions) != 1 { + return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx length of ExtensionOptions should be 1") + } + + authInfo := protoTx.AuthInfo + if len(authInfo.SignerInfos) > 0 { + return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx AuthInfo SignerInfos should be empty") + } + + if authInfo.Fee.Payer != "" || authInfo.Fee.Granter != "" { + return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx AuthInfo Fee payer and granter should be empty") + } + + sigs := protoTx.Signatures + if len(sigs) > 0 { + return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx Signatures should be empty") + } + + txFee := sdk.Coins{} + txGasLimit := uint64(0) + + evmParams := vbd.EvmKeeper.GetParams(ctx) + baseFee := vbd.EvmKeeper.GetBaseFee(ctx) + enableCreate := evmParams.GetEnableCreate() + enableCall := evmParams.GetEnableCall() + evmDenom := evmParams.GetEvmDenom() + + for _, msg := range protoTx.GetMsgs() { + msgEthTx, ok := msg.(*evm.MsgEthereumTx) + if !ok { + return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + } + + // Validate `From` field + if msgEthTx.From != "" { + return ctx, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid From %s, expect empty string", msgEthTx.From) + } + + txGasLimit += msgEthTx.GetGas() + + txData, err := evm.UnpackTxData(msgEthTx.Data) + if err != nil { + return ctx, errorsmod.Wrap(err, "failed to unpack MsgEthereumTx Data") + } + + // return error if contract creation or call are disabled through governance + if !enableCreate && txData.GetTo() == nil { + return ctx, errorsmod.Wrap(evm.ErrCreateDisabled, "failed to create new contract") + } else if !enableCall && txData.GetTo() != nil { + return ctx, errorsmod.Wrap(evm.ErrCallDisabled, "failed to call contract") + } + + if baseFee == nil && txData.TxType() == gethcore.DynamicFeeTxType { + return ctx, errorsmod.Wrap(gethcore.ErrTxTypeNotSupported, "dynamic fee tx not supported") + } + + txFee = txFee.Add(sdk.Coin{Denom: evmDenom, Amount: sdkmath.NewIntFromBigInt(txData.Fee())}) + } + + if !authInfo.Fee.Amount.IsEqual(txFee) { + return ctx, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid AuthInfo Fee Amount (%s != %s)", authInfo.Fee.Amount, txFee) + } + + if authInfo.Fee.GasLimit != txGasLimit { + return ctx, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid AuthInfo Fee GasLimit (%d != %d)", authInfo.Fee.GasLimit, txGasLimit) + } + + return next(ctx, tx, simulate) +} From 9b20a6cba382fbe246b61aebb3280ae1d38d7fc7 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Fri, 7 Jun 2024 18:37:34 +0400 Subject: [PATCH 05/13] test(evm): ante handler emit event coverage --- app/evmante_emit_event.go | 21 ++++++--- app/evmante_emit_event_test.go | 78 ++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 app/evmante_emit_event_test.go diff --git a/app/evmante_emit_event.go b/app/evmante_emit_event.go index 81c24fcb2..834bdb056 100644 --- a/app/evmante_emit_event.go +++ b/app/evmante_emit_event.go @@ -32,17 +32,26 @@ func (eeed EthEmitEventDecorator) AnteHandle( for i, msg := range tx.GetMsgs() { msgEthTx, ok := msg.(*evm.MsgEthereumTx) if !ok { - return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + return ctx, errorsmod.Wrapf( + errortypes.ErrUnknownRequest, + "invalid message type %T, expected %T", + msg, (*evm.MsgEthereumTx)(nil), + ) } // 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( - evm.EventTypeEthereumTx, - sdk.NewAttribute(evm.AttributeKeyEthereumTxHash, msgEthTx.Hash), - sdk.NewAttribute(evm.AttributeKeyTxIndex, strconv.FormatUint(txIndex+uint64(i), 10)), // #nosec G701 - )) + ctx.EventManager().EmitEvent( + sdk.NewEvent( + evm.EventTypeEthereumTx, + sdk.NewAttribute(evm.AttributeKeyEthereumTxHash, msgEthTx.Hash), + sdk.NewAttribute( + evm.AttributeKeyTxIndex, strconv.FormatUint(txIndex+uint64(i), + 10, + ), + ), // #nosec G701 + )) } return next(ctx, tx, simulate) diff --git a/app/evmante_emit_event_test.go b/app/evmante_emit_event_test.go new file mode 100644 index 000000000..727444949 --- /dev/null +++ b/app/evmante_emit_event_test.go @@ -0,0 +1,78 @@ +package app_test + +import ( + "github.com/NibiruChain/nibiru/x/evm" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/x/evm/evmtest" + tf "github.com/NibiruChain/nibiru/x/tokenfactory/types" +) + +func (s *TestSuite) TestEthEmitEventDecorator() { + testCases := []struct { + name string + txSetup func(deps *evmtest.TestDeps) sdk.Tx + wantErr string + }{ + { + name: "sad: non ethereum tx", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return legacytx.StdTx{ + Msgs: []sdk.Msg{ + &tf.MsgMint{}, + }, + } + }, + wantErr: "invalid message", + }, + { + name: "happy: eth tx emitted event", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + tx := happyCreateContractTx(deps) + return tx + }, + wantErr: "", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + stateDB := deps.StateDB() + anteDec := app.NewEthEmitEventDecorator(deps.Chain.AppKeepers) + + tx := tc.txSetup(&deps) + s.Require().NoError(stateDB.Commit()) + + _, err := anteDec.AnteHandle( + deps.Ctx, tx, false, NextNoOpAnteHandler, + ) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + events := deps.Ctx.EventManager().Events() + + s.Require().Greater(len(events), 0) + event := events[len(events)-1] + s.Require().Equal(evm.EventTypeEthereumTx, event.Type) + + // Convert tx to msg to get hash + txMsg, ok := tx.GetMsgs()[0].(*evm.MsgEthereumTx) + s.Require().True(ok) + + // TX hash attr must present + attr, ok := event.GetAttribute(evm.AttributeKeyEthereumTxHash) + s.Require().True(ok, "tx hash attribute not found") + s.Require().Equal(txMsg.Hash, attr.Value) + + // TX index attr must present + attr, ok = event.GetAttribute(evm.AttributeKeyTxIndex) + s.Require().True(ok, "tx index attribute not found") + s.Require().Equal("0", attr.Value) + }) + } +} From 5a237b6f303cb6cf0a3e06c7c8bfa5287551c4fa Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Fri, 7 Jun 2024 20:45:59 +0400 Subject: [PATCH 06/13] test(evm): evmante hanlers emit_event, setup_ctx and validate_basic --- app/evmante_emit_event.go | 3 +- app/evmante_emit_event_test.go | 3 +- app/evmante_setup_ctx_test.go | 5 +- app/evmante_validate_basic.go | 72 +++++++++++++++++++++----- app/evmante_validate_basic_test.go | 82 ++++++++++++++++++++++++++++++ 5 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 app/evmante_validate_basic_test.go diff --git a/app/evmante_emit_event.go b/app/evmante_emit_event.go index 834bdb056..b621a8dbe 100644 --- a/app/evmante_emit_event.go +++ b/app/evmante_emit_event.go @@ -5,9 +5,10 @@ import ( "strconv" errorsmod "cosmossdk.io/errors" - "github.com/NibiruChain/nibiru/x/evm" sdk "github.com/cosmos/cosmos-sdk/types" errortypes "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/NibiruChain/nibiru/x/evm" ) // EthEmitEventDecorator emit events in ante handler in case of tx execution failed (out of block gas limit). diff --git a/app/evmante_emit_event_test.go b/app/evmante_emit_event_test.go index 727444949..39aa1351f 100644 --- a/app/evmante_emit_event_test.go +++ b/app/evmante_emit_event_test.go @@ -1,10 +1,11 @@ package app_test import ( - "github.com/NibiruChain/nibiru/x/evm" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" + "github.com/NibiruChain/nibiru/x/evm" + "github.com/NibiruChain/nibiru/app" "github.com/NibiruChain/nibiru/x/evm/evmtest" tf "github.com/NibiruChain/nibiru/x/tokenfactory/types" diff --git a/app/evmante_setup_ctx_test.go b/app/evmante_setup_ctx_test.go index 5a3597414..132ed8f1a 100644 --- a/app/evmante_setup_ctx_test.go +++ b/app/evmante_setup_ctx_test.go @@ -3,10 +3,11 @@ package app_test import ( "math" - "github.com/NibiruChain/nibiru/app" - "github.com/NibiruChain/nibiru/x/evm/evmtest" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/x/evm/evmtest" ) func (s *TestSuite) TestEthSetupContextDecorator() { diff --git a/app/evmante_validate_basic.go b/app/evmante_validate_basic.go index 292edf3d9..2c006485a 100644 --- a/app/evmante_validate_basic.go +++ b/app/evmante_validate_basic.go @@ -42,7 +42,11 @@ func (vbd EthValidateBasicDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simu // since we will only verify the signature against the hash of the MsgEthereumTx.Data wrapperTx, ok := tx.(protoTxProvider) if !ok { - return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid tx type %T, didn't implement interface protoTxProvider", tx) + return ctx, errorsmod.Wrapf( + errortypes.ErrUnknownRequest, + "invalid tx type %T, didn't implement interface protoTxProvider", + tx, + ) } protoTx := wrapperTx.GetProtoTx() @@ -53,21 +57,33 @@ func (vbd EthValidateBasicDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simu } if len(body.ExtensionOptions) != 1 { - return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx length of ExtensionOptions should be 1") + return ctx, errorsmod.Wrap( + errortypes.ErrInvalidRequest, + "for eth tx length of ExtensionOptions should be 1", + ) } authInfo := protoTx.AuthInfo if len(authInfo.SignerInfos) > 0 { - return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx AuthInfo SignerInfos should be empty") + return ctx, errorsmod.Wrap( + errortypes.ErrInvalidRequest, + "for eth tx AuthInfo SignerInfos should be empty", + ) } if authInfo.Fee.Payer != "" || authInfo.Fee.Granter != "" { - return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx AuthInfo Fee payer and granter should be empty") + return ctx, errorsmod.Wrap( + errortypes.ErrInvalidRequest, + "for eth tx AuthInfo Fee payer and granter should be empty", + ) } sigs := protoTx.Signatures if len(sigs) > 0 { - return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx Signatures should be empty") + return ctx, errorsmod.Wrap( + errortypes.ErrInvalidRequest, + "for eth tx Signatures should be empty", + ) } txFee := sdk.Coins{} @@ -82,12 +98,18 @@ func (vbd EthValidateBasicDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simu for _, msg := range protoTx.GetMsgs() { msgEthTx, ok := msg.(*evm.MsgEthereumTx) if !ok { - return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + return ctx, errorsmod.Wrapf( + errortypes.ErrUnknownRequest, + "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil), + ) } // Validate `From` field if msgEthTx.From != "" { - return ctx, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid From %s, expect empty string", msgEthTx.From) + return ctx, errorsmod.Wrapf( + errortypes.ErrInvalidRequest, + "invalid From %s, expect empty string", msgEthTx.From, + ) } txGasLimit += msgEthTx.GetGas() @@ -99,24 +121,48 @@ func (vbd EthValidateBasicDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simu // return error if contract creation or call are disabled through governance if !enableCreate && txData.GetTo() == nil { - return ctx, errorsmod.Wrap(evm.ErrCreateDisabled, "failed to create new contract") + return ctx, errorsmod.Wrap( + evm.ErrCreateDisabled, + "failed to create new contract", + ) } else if !enableCall && txData.GetTo() != nil { - return ctx, errorsmod.Wrap(evm.ErrCallDisabled, "failed to call contract") + return ctx, errorsmod.Wrap( + evm.ErrCallDisabled, + "failed to call contract", + ) } if baseFee == nil && txData.TxType() == gethcore.DynamicFeeTxType { - return ctx, errorsmod.Wrap(gethcore.ErrTxTypeNotSupported, "dynamic fee tx not supported") + return ctx, errorsmod.Wrap( + gethcore.ErrTxTypeNotSupported, + "dynamic fee tx not supported", + ) } - txFee = txFee.Add(sdk.Coin{Denom: evmDenom, Amount: sdkmath.NewIntFromBigInt(txData.Fee())}) + txFee = txFee.Add( + sdk.Coin{ + Denom: evmDenom, + Amount: sdkmath.NewIntFromBigInt(txData.Fee()), + }, + ) } if !authInfo.Fee.Amount.IsEqual(txFee) { - return ctx, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid AuthInfo Fee Amount (%s != %s)", authInfo.Fee.Amount, txFee) + return ctx, errorsmod.Wrapf( + errortypes.ErrInvalidRequest, + "invalid AuthInfo Fee Amount (%s != %s)", + authInfo.Fee.Amount, + txFee, + ) } if authInfo.Fee.GasLimit != txGasLimit { - return ctx, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid AuthInfo Fee GasLimit (%d != %d)", authInfo.Fee.GasLimit, txGasLimit) + return ctx, errorsmod.Wrapf( + errortypes.ErrInvalidRequest, + "invalid AuthInfo Fee GasLimit (%d != %d)", + authInfo.Fee.GasLimit, + txGasLimit, + ) } return next(ctx, tx, simulate) diff --git a/app/evmante_validate_basic_test.go b/app/evmante_validate_basic_test.go new file mode 100644 index 000000000..8e8786440 --- /dev/null +++ b/app/evmante_validate_basic_test.go @@ -0,0 +1,82 @@ +package app_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +func (s *TestSuite) TestEthValidateBasicDecorator() { + testCases := []struct { + name string + ctxSetup func(deps *evmtest.TestDeps) + txSetup func(deps *evmtest.TestDeps) sdk.Tx + wantErr string + }{ + { + name: "sad: unsigned уер tx should fail chain id validation", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return happyCreateContractTx(deps) + }, + wantErr: "invalid chain-id", + }, + { + name: "happy: ctx recheck should ignore validation", + ctxSetup: func(deps *evmtest.TestDeps) { + deps.Ctx = deps.Ctx.WithIsReCheckTx(true) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return happyCreateContractTx(deps) + }, + wantErr: "", + }, + { + name: "sad: tx not implementing protoTxProvider", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + tx := happyCreateContractTx(deps) + gethSigner := deps.Sender.GethSigner(InvalidChainID) + keyringSigner := deps.Sender.KeyringSigner + err := tx.Sign(gethSigner, keyringSigner) + s.Require().NoError(err) + return tx + }, + wantErr: "didn't implement interface protoTxProvider", + }, + { + name: "happy: signed ethereum tx should pass validation", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + tx, err := happyCreateContractTx(deps).BuildTx(txBuilder, eth.EthBaseDenom) + s.Require().NoError(err) + return tx + }, + wantErr: "", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + stateDB := deps.StateDB() + anteDec := app.NewEthValidateBasicDecorator(deps.Chain.AppKeepers) + + tx := tc.txSetup(&deps) + s.Require().NoError(stateDB.Commit()) + + if tc.ctxSetup != nil { + tc.ctxSetup(&deps) + } + + _, err := anteDec.AnteHandle( + deps.Ctx, tx, false, NextNoOpAnteHandler, + ) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + }) + } +} From f807e6f3102bdd2b23fb6a2b0280bf3b1313438d Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Mon, 10 Jun 2024 14:02:11 +0200 Subject: [PATCH 07/13] fix: lint --- CHANGELOG.md | 2 +- app/evmante_validate_basic_test.go | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b6eadbce..7f87a4cb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,7 +70,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#1907](https://github.com/NibiruChain/nibiru/pull/1907) - test(evm): grpc_query full coverage - [#1909](https://github.com/NibiruChain/nibiru/pull/1909) - chore(evm): set is_london true by default and removed from config - [#1911](https://github.com/NibiruChain/nibiru/pull/1911) - chore(evm): simplified config by removing old eth forks -- [#1912](https://github.com/NibiruChain/nibiru/pull/1912) - test(evm): unit tests for evm_ante_sigverify +- [#1912](https://github.com/NibiruChain/nibiru/pull/1912) - test(evm): unit tests for evm_ante - [#1914](https://github.com/NibiruChain/nibiru/pull/1914) - refactor(evm): Remove dead code and document non-EVM ante handler- [#1917](https://github.com/NibiruChain/nibiru/pull/1917) - test(e2e-evm): TypeScript support. Type generation from compiled contracts. Formatter for TS code. - [#1917](https://github.com/NibiruChain/nibiru/pull/1917) - test(e2e-evm): TypeScript support. Type generation from compiled contracts. Formatter for TS code. diff --git a/app/evmante_validate_basic_test.go b/app/evmante_validate_basic_test.go index f952ec33d..ae11eb68c 100644 --- a/app/evmante_validate_basic_test.go +++ b/app/evmante_validate_basic_test.go @@ -3,17 +3,18 @@ package app_test import ( "math/big" - "github.com/NibiruChain/nibiru/app" - "github.com/NibiruChain/nibiru/eth" - "github.com/NibiruChain/nibiru/x/common/testutil" - "github.com/NibiruChain/nibiru/x/evm" - "github.com/NibiruChain/nibiru/x/evm/evmtest" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/tx/signing" authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/ethereum/go-ethereum/common" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/x/common/testutil" + "github.com/NibiruChain/nibiru/x/evm" + "github.com/NibiruChain/nibiru/x/evm/evmtest" ) func (s *TestSuite) TestEthValidateBasicDecorator() { @@ -115,6 +116,7 @@ func (s *TestSuite) TestEthValidateBasicDecorator() { gethSigner := deps.Sender.GethSigner(deps.Chain.EvmKeeper.EthChainID(deps.Ctx)) keyringSigner := deps.Sender.KeyringSigner err = txMsg.Sign(gethSigner, keyringSigner) + s.Require().NoError(err) tx, err := txMsg.BuildTx(txBuilder, eth.EthBaseDenom) s.Require().NoError(err) @@ -273,7 +275,10 @@ func buildTx( option, _ := codectypes.NewAnyWithValue(&evm.ExtensionOptionsEthereumTx{}) txBuilder.SetExtensionOptions(option) } - txBuilder.SetMsgs(msg) + err := txBuilder.SetMsgs(msg) + if err != nil { + panic(err) + } txBuilder.SetGasLimit(gasLimit) txBuilder.SetFeeAmount(fees) From 93ca58357241e656c89660045b8b78fa36ceda9c Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Mon, 10 Jun 2024 18:06:04 +0200 Subject: [PATCH 08/13] test(evm): evmante fee checker coverage --- app/evmante_fee_checker.go | 28 ++++++++-- app/evmante_fee_checker_test.go | 97 +++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 app/evmante_fee_checker_test.go diff --git a/app/evmante_fee_checker.go b/app/evmante_fee_checker.go index 5299e0eb1..5bc5c50f2 100644 --- a/app/evmante_fee_checker.go +++ b/app/evmante_fee_checker.go @@ -26,7 +26,9 @@ import ( // - Tx priority is set to `effectiveGasPrice / DefaultPriorityReduction`. func NewDynamicFeeChecker(k evmkeeper.Keeper) ante.TxFeeChecker { return func(ctx sdk.Context, feeTx sdk.FeeTx) (sdk.Coins, int64, error) { - // 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 + // 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) @@ -51,7 +53,10 @@ func NewDynamicFeeChecker(k evmkeeper.Keeper) ante.TxFeeChecker { // priority fee cannot be negative if maxPriorityPrice.IsNegative() { - return nil, 0, errors.Wrapf(errortypes.ErrInsufficientFee, "max priority price cannot be negative") + return nil, 0, errors.Wrapf( + errortypes.ErrInsufficientFee, + "max priority price cannot be negative", + ) } gas := feeTx.GetGas() @@ -62,11 +67,21 @@ func NewDynamicFeeChecker(k evmkeeper.Keeper) ante.TxFeeChecker { baseFeeInt := sdkmath.NewIntFromBigInt(baseFee) if feeCap.LT(baseFeeInt) { - return nil, 0, errors.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) + return nil, 0, errors.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(evm.EffectiveGasPrice(baseFeeInt.BigInt(), feeCap.BigInt(), maxPriorityPrice.BigInt())) + effectivePrice := sdkmath.NewIntFromBigInt( + evm.EffectiveGasPrice(baseFeeInt.BigInt(), feeCap.BigInt(), maxPriorityPrice.BigInt()), + ) // NOTE: create a new coins slice without having to validate the denom effectiveFee := sdk.Coins{ @@ -109,7 +124,10 @@ func checkTxFeeWithValidatorMinGasPrices(ctx sdk.Context, tx sdk.FeeTx) (sdk.Coi } if !feeCoins.IsAnyGTE(requiredFees) { - return nil, 0, errors.Wrapf(errortypes.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins, requiredFees) + return nil, 0, errors.Wrapf( + errortypes.ErrInsufficientFee, + "insufficient fees; got: %s required: %s", feeCoins, requiredFees, + ) } } diff --git a/app/evmante_fee_checker_test.go b/app/evmante_fee_checker_test.go new file mode 100644 index 000000000..d4961f011 --- /dev/null +++ b/app/evmante_fee_checker_test.go @@ -0,0 +1,97 @@ +package app_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +func (s *TestSuite) TestNewDynamicFeeChecker() { + testCases := []struct { + name string + txSetup func(deps *evmtest.TestDeps) sdk.FeeTx + ctxSetup func(deps *evmtest.TestDeps) + wantErr string + wantFee int64 + wantPriority int64 + }{ + { + name: "happy: genesis tx with sufficient fee", + ctxSetup: func(deps *evmtest.TestDeps) { + gasPrice := sdk.NewInt64Coin("unibi", 1) + deps.Ctx = deps.Ctx. + WithBlockHeight(0). + WithMinGasPrices( + sdk.NewDecCoins(sdk.NewDecCoinFromCoin(gasPrice)), + ). + WithIsCheckTx(true) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.FeeTx { + txMsg := happyCreateContractTx(deps) + txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + tx, err := txMsg.BuildTx(txBuilder, eth.EthBaseDenom) + s.Require().NoError(err) + return tx + }, + wantErr: "", + wantFee: gasLimitCreateContract().Int64(), + wantPriority: 0, + }, + { + name: "sad: genesis tx insufficient fee", + ctxSetup: func(deps *evmtest.TestDeps) { + gasPrice := sdk.NewInt64Coin("unibi", 2) + deps.Ctx = deps.Ctx. + WithBlockHeight(0). + WithMinGasPrices( + sdk.NewDecCoins(sdk.NewDecCoinFromCoin(gasPrice)), + ). + WithIsCheckTx(true) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.FeeTx { + txMsg := happyCreateContractTx(deps) + txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + tx, err := txMsg.BuildTx(txBuilder, eth.EthBaseDenom) + s.Require().NoError(err) + return tx + }, + wantErr: "insufficient fee", + }, + { + name: "happy: tx with sufficient fee", + txSetup: func(deps *evmtest.TestDeps) sdk.FeeTx { + txMsg := happyCreateContractTx(deps) + txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + tx, err := txMsg.BuildTx(txBuilder, eth.EthBaseDenom) + s.Require().NoError(err) + return tx + }, + wantErr: "", + wantFee: gasLimitCreateContract().Int64(), + wantPriority: 0, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + checker := app.NewDynamicFeeChecker(deps.K) + + if tc.ctxSetup != nil { + tc.ctxSetup(&deps) + } + + fee, priority, err := checker(deps.Ctx, tc.txSetup(&deps)) + + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + s.Require().Equal(tc.wantFee, fee.AmountOf("unibi").Int64()) + s.Require().Equal(tc.wantPriority, priority) + }) + } +} From 52ef738dd6abb39746d3930305ccccec1fafeac9 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Mon, 10 Jun 2024 18:24:42 +0200 Subject: [PATCH 09/13] test(evm): evmante fees test coverage --- app/evmante_fees.go | 4 +- app/evmante_fees_test.go | 103 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 app/evmante_fees_test.go diff --git a/app/evmante_fees.go b/app/evmante_fees.go index da9d9d61c..0e180d7e5 100644 --- a/app/evmante_fees.go +++ b/app/evmante_fees.go @@ -85,7 +85,9 @@ func (empd EthMinGasPriceDecorator) AnteHandle( if fee.LT(requiredFee) { return ctx, errors.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 + "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)", fee.TruncateInt().String(), requiredFee.TruncateInt().String(), ) } diff --git a/app/evmante_fees_test.go b/app/evmante_fees_test.go new file mode 100644 index 000000000..f2ac7238b --- /dev/null +++ b/app/evmante_fees_test.go @@ -0,0 +1,103 @@ +package app_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +func (s *TestSuite) TestEthMinGasPriceDecorator() { + testCases := []struct { + name string + txSetup func(deps *evmtest.TestDeps) sdk.Tx + ctxSetup func(deps *evmtest.TestDeps) + wantErr string + }{ + { + name: "happy: min gas price is 0", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + tx := happyCreateContractTx(deps) + return tx + }, + wantErr: "", + }, + { + name: "happy: min gas price is not zero, sufficient fee", + ctxSetup: func(deps *evmtest.TestDeps) { + gasPrice := sdk.NewInt64Coin("unibi", 1) + deps.Ctx = deps.Ctx. + WithMinGasPrices( + sdk.NewDecCoins(sdk.NewDecCoinFromCoin(gasPrice)), + ). + WithIsCheckTx(true) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + tx := happyCreateContractTx(deps) + return tx + }, + wantErr: "", + }, + { + name: "sad: insufficient fee", + ctxSetup: func(deps *evmtest.TestDeps) { + gasPrice := sdk.NewInt64Coin("unibi", 2) + deps.Ctx = deps.Ctx. + WithMinGasPrices( + sdk.NewDecCoins(sdk.NewDecCoinFromCoin(gasPrice)), + ). + WithIsCheckTx(true) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + tx := happyCreateContractTx(deps) + return tx + }, + wantErr: "insufficient fee", + }, + { + name: "sad: tx with non evm message", + ctxSetup: func(deps *evmtest.TestDeps) { + gasPrice := sdk.NewInt64Coin("unibi", 1) + deps.Ctx = deps.Ctx. + WithMinGasPrices( + sdk.NewDecCoins(sdk.NewDecCoinFromCoin(gasPrice)), + ). + WithIsCheckTx(true) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + gasLimit := uint64(10) + fees := sdk.NewCoins(sdk.NewInt64Coin("unibi", int64(gasLimit))) + msg := &banktypes.MsgSend{ + FromAddress: deps.Sender.NibiruAddr.String(), + ToAddress: evmtest.NewEthAccInfo().NibiruAddr.String(), + Amount: sdk.NewCoins(sdk.NewInt64Coin("unibi", 1)), + } + return buildTx(deps, true, msg, gasLimit, fees) + }, + wantErr: "invalid message", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + anteDec := app.NewEthMinGasPriceDecorator(deps.Chain.AppKeepers) + + tx := tc.txSetup(&deps) + + if tc.ctxSetup != nil { + tc.ctxSetup(&deps) + } + + _, err := anteDec.AnteHandle( + deps.Ctx, tx, false, NextNoOpAnteHandler, + ) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + }) + } +} From 89cc59188e89a496f98ac19691bad49234c32e50 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Mon, 10 Jun 2024 18:57:57 +0200 Subject: [PATCH 10/13] test(evm): split evmante handlers 1 handler 1 file --- app/evmante_can_transfer.go | 94 +++++ app/evmante_eth.go | 386 ------------------ app/evmante_gas_consume.go | 182 +++++++++ app/evmante_increment_sender_seq.go | 73 ++++ app/evmante_verify_eth_acc.go | 86 ++++ ...test.go => evmante_verify_eth_acc_test.go} | 3 - 6 files changed, 435 insertions(+), 389 deletions(-) create mode 100644 app/evmante_can_transfer.go delete mode 100644 app/evmante_eth.go create mode 100644 app/evmante_gas_consume.go create mode 100644 app/evmante_increment_sender_seq.go create mode 100644 app/evmante_verify_eth_acc.go rename app/{evmante_test.go => evmante_verify_eth_acc_test.go} (98%) diff --git a/app/evmante_can_transfer.go b/app/evmante_can_transfer.go new file mode 100644 index 000000000..026a46d26 --- /dev/null +++ b/app/evmante_can_transfer.go @@ -0,0 +1,94 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package app + +import ( + "math/big" + + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/NibiruChain/nibiru/x/evm" + "github.com/NibiruChain/nibiru/x/evm/statedb" + + gethcommon "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" +) + +// CanTransferDecorator checks if the sender is allowed to transfer funds according to the EVM block +// context rules. +type CanTransferDecorator struct { + AppKeepers +} + +// NewCanTransferDecorator creates a new CanTransferDecorator instance. +func NewCanTransferDecorator(k AppKeepers) CanTransferDecorator { + return CanTransferDecorator{ + AppKeepers: k, + } +} + +// AnteHandle creates an EVM from the message and calls the BlockContext CanTransfer function to +// see if the address can execute the transaction. +func (ctd CanTransferDecorator) AnteHandle( + ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, +) (sdk.Context, error) { + params := ctd.EvmKeeper.GetParams(ctx) + ethCfg := evm.EthereumConfig(ctd.EvmKeeper.EthChainID(ctx)) + signer := gethcore.MakeSigner(ethCfg, big.NewInt(ctx.BlockHeight())) + + for _, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evm.MsgEthereumTx) + if !ok { + return ctx, errors.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + } + + baseFee := ctd.EvmKeeper.GetBaseFee(ctx) + + coreMsg, err := msgEthTx.AsMessage(signer, baseFee) + if err != nil { + return ctx, errors.Wrapf( + err, + "failed to create an ethereum core.Message from signer %T", signer, + ) + } + + if baseFee == nil { + return ctx, errors.Wrap( + evm.ErrInvalidBaseFee, + "base fee is supported but evm block context value is nil", + ) + } + if coreMsg.GasFeeCap().Cmp(baseFee) < 0 { + return ctx, errors.Wrapf( + errortypes.ErrInsufficientFee, + "max fee per gas less than block base fee (%s < %s)", + coreMsg.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: gethcommon.Address{}, + BaseFee: baseFee, + } + + stateDB := statedb.New(ctx, &ctd.EvmKeeper, statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes()))) + evm := ctd.EvmKeeper.NewEVM(ctx, coreMsg, cfg, evm.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 coreMsg.Value().Sign() > 0 && !evm.Context.CanTransfer(stateDB, coreMsg.From(), coreMsg.Value()) { + return ctx, errors.Wrapf( + errortypes.ErrInsufficientFunds, + "failed to transfer %s from address %s using the EVM block context transfer function", + coreMsg.Value(), + coreMsg.From(), + ) + } + } + + return next(ctx, tx, simulate) +} diff --git a/app/evmante_eth.go b/app/evmante_eth.go deleted file mode 100644 index 4cfd14d02..000000000 --- a/app/evmante_eth.go +++ /dev/null @@ -1,386 +0,0 @@ -// Copyright (c) 2023-2024 Nibi, Inc. -package app - -import ( - "math" - "math/big" - - "cosmossdk.io/errors" - sdkmath "cosmossdk.io/math" - - sdk "github.com/cosmos/cosmos-sdk/types" - errortypes "github.com/cosmos/cosmos-sdk/types/errors" - - "github.com/NibiruChain/nibiru/eth" - "github.com/NibiruChain/nibiru/x/evm" - "github.com/NibiruChain/nibiru/x/evm/keeper" - "github.com/NibiruChain/nibiru/x/evm/statedb" - - gethcommon "github.com/ethereum/go-ethereum/common" - gethcore "github.com/ethereum/go-ethereum/core/types" -) - -var ( - _ sdk.AnteDecorator = (*AnteDecEthGasConsume)(nil) - _ sdk.AnteDecorator = (*AnteDecVerifyEthAcc)(nil) -) - -// AnteDecVerifyEthAcc validates an account balance checks -type AnteDecVerifyEthAcc struct { - AppKeepers -} - -// NewAnteDecVerifyEthAcc creates a new EthAccountVerificationDecorator -func NewAnteDecVerifyEthAcc(k AppKeepers) AnteDecVerifyEthAcc { - return AnteDecVerifyEthAcc{ - AppKeepers: k, - } -} - -// AnteHandle validates checks that the sender 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 AnteHandler decorator will fail if: -// - any of the msgs is not a MsgEthereumTx -// - from address is empty -// - account balance is lower than the transaction cost -func (anteDec AnteDecVerifyEthAcc) AnteHandle( - ctx sdk.Context, - tx sdk.Tx, - simulate bool, - next sdk.AnteHandler, -) (newCtx sdk.Context, err error) { - if !ctx.IsCheckTx() { - return next(ctx, tx, simulate) - } - - for i, msg := range tx.GetMsgs() { - msgEthTx, ok := msg.(*evm.MsgEthereumTx) - if !ok { - return ctx, errors.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) - } - - txData, err := evm.UnpackTxData(msgEthTx.Data) - if err != nil { - return ctx, errors.Wrapf(err, "failed to unpack tx data any for tx %d", i) - } - - // sender address should be in the tx cache from the previous AnteHandle call - from := msgEthTx.GetFrom() - if from.Empty() { - return ctx, errors.Wrap(errortypes.ErrInvalidAddress, "from address cannot be empty") - } - - // check whether the sender address is EOA - fromAddr := gethcommon.BytesToAddress(from) - acct := anteDec.EvmKeeper.GetAccount(ctx, fromAddr) - - if acct == nil { - acc := anteDec.AccountKeeper.NewAccountWithAddress(ctx, from) - anteDec.AccountKeeper.SetAccount(ctx, acc) - acct = statedb.NewEmptyAccount() - } else if acct.IsContract() { - return ctx, errors.Wrapf(errortypes.ErrInvalidType, - "the sender is not EOA: address %s, codeHash <%s>", fromAddr, acct.CodeHash) - } - - if err := keeper.CheckSenderBalance(sdkmath.NewIntFromBigInt(acct.Balance), txData); err != nil { - return ctx, errors.Wrap(err, "failed to check sender balance") - } - } - return next(ctx, tx, simulate) -} - -// AnteDecEthGasConsume validates enough intrinsic gas for the transaction and -// gas consumption. -type AnteDecEthGasConsume struct { - AppKeepers - // bankKeeper anteutils.BankKeeper - // distributionKeeper anteutils.DistributionKeeper - // evmKeeper EVMKeeper - // stakingKeeper anteutils.StakingKeeper - maxGasWanted uint64 -} - -// NewAnteDecEthGasConsume creates a new EthGasConsumeDecorator -func NewAnteDecEthGasConsume( - keepers AppKeepers, - maxGasWanted uint64, -) AnteDecEthGasConsume { - return AnteDecEthGasConsume{ - AppKeepers: keepers, - maxGasWanted: maxGasWanted, - } -} - -// AnteHandle validates that the Ethereum tx message has enough to cover -// intrinsic gas (during CheckTx only) and that the sender has enough balance to -// pay for the gas cost. If the balance is not sufficient, it will be attempted -// to withdraw enough staking rewards for the payment. -// -// Intrinsic gas for a transaction is the amount of gas that the transaction uses -// before the transaction is executed. The gas is a constant value plus any cost -// incurred by additional bytes of data supplied with the transaction. -// -// This AnteHandler decorator will fail if: -// - the message is not a MsgEthereumTx -// - sender account cannot be found -// - transaction's gas limit is lower than the intrinsic gas -// - user has neither enough balance nor staking rewards to deduct the transaction fees (gas_limit * gas_price) -// - transaction or block gas meter runs out of gas -// - sets the gas meter limit -// - gas limit is greater than the block gas meter limit -func (anteDec AnteDecEthGasConsume) AnteHandle( - ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, -) (sdk.Context, error) { - gasWanted := uint64(0) - if ctx.IsReCheckTx() { - // Then, the limit for gas consumed was already checked during CheckTx so - // there's no need to verify it again during ReCheckTx - // - // Use new context with gasWanted = 0 - // Otherwise, there's an error on txmempool.postCheck (tendermint) - // that is not bubbled up. Thus, the Tx never runs on DeliverMode - // Error: "gas wanted -1 is negative" - newCtx := ctx.WithGasMeter(eth.NewInfiniteGasMeterWithLimit(gasWanted)) - return next(newCtx, tx, simulate) - } - - evmParams := anteDec.EvmKeeper.GetParams(ctx) - evmDenom := evmParams.GetEvmDenom() - - var events sdk.Events - - // Use the lowest priority of all the messages as the final one. - minPriority := int64(math.MaxInt64) - baseFee := anteDec.EvmKeeper.GetBaseFee(ctx) - - for _, msg := range tx.GetMsgs() { - msgEthTx, ok := msg.(*evm.MsgEthereumTx) - if !ok { - return ctx, errors.Wrapf( - errortypes.ErrUnknownRequest, - "invalid message type %T, expected %T", - msg, (*evm.MsgEthereumTx)(nil), - ) - } - from := msgEthTx.GetFrom() - - txData, err := evm.UnpackTxData(msgEthTx.Data) - if err != nil { - return ctx, errors.Wrap(err, "failed to unpack tx data") - } - - if ctx.IsCheckTx() && anteDec.maxGasWanted != 0 { - // We can't trust the tx gas limit, because we'll refund the unused gas. - if txData.GetGas() > anteDec.maxGasWanted { - gasWanted += anteDec.maxGasWanted - } else { - gasWanted += txData.GetGas() - } - } else { - gasWanted += txData.GetGas() - } - - fees, err := keeper.VerifyFee(txData, evmDenom, baseFee, ctx.IsCheckTx()) - if err != nil { - return ctx, errors.Wrapf(err, "failed to verify the fees") - } - - if err = anteDec.deductFee(ctx, fees, from); err != nil { - return ctx, err - } - - events = append(events, - sdk.NewEvent( - sdk.EventTypeTx, - sdk.NewAttribute(sdk.AttributeKeyFee, fees.String()), - ), - ) - - priority := evm.GetTxPriority(txData, baseFee) - - if priority < minPriority { - minPriority = priority - } - } - - ctx.EventManager().EmitEvents(events) - - blockGasLimit := eth.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, errors.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. - newCtx := ctx. - WithGasMeter(eth.NewInfiniteGasMeterWithLimit(gasWanted)). - WithPriority(minPriority) - - // we know that we have enough gas on the pool to cover the intrinsic gas - return next(newCtx, tx, simulate) -} - -// 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 (anteDec AnteDecEthGasConsume) deductFee(ctx sdk.Context, fees sdk.Coins, feePayer sdk.AccAddress) error { - if fees.IsZero() { - return nil - } - - // If the account balance is not sufficient, try to withdraw enough staking rewards - - if err := anteDec.EvmKeeper.DeductTxCostsFromUserBalance(ctx, fees, gethcommon.BytesToAddress(feePayer)); err != nil { - return errors.Wrapf(err, "failed to deduct transaction costs from user balance") - } - return nil -} - -// CanTransferDecorator checks if the sender is allowed to transfer funds according to the EVM block -// context rules. -type CanTransferDecorator struct { - AppKeepers -} - -// NewCanTransferDecorator creates a new CanTransferDecorator instance. -func NewCanTransferDecorator(k AppKeepers) CanTransferDecorator { - return CanTransferDecorator{ - AppKeepers: k, - } -} - -// AnteHandle creates an EVM from the message and calls the BlockContext CanTransfer function to -// see if the address can execute the transaction. -func (ctd CanTransferDecorator) AnteHandle( - ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, -) (sdk.Context, error) { - params := ctd.EvmKeeper.GetParams(ctx) - ethCfg := evm.EthereumConfig(ctd.EvmKeeper.EthChainID(ctx)) - signer := gethcore.MakeSigner(ethCfg, big.NewInt(ctx.BlockHeight())) - - for _, msg := range tx.GetMsgs() { - msgEthTx, ok := msg.(*evm.MsgEthereumTx) - if !ok { - return ctx, errors.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) - } - - baseFee := ctd.EvmKeeper.GetBaseFee(ctx) - - coreMsg, err := msgEthTx.AsMessage(signer, baseFee) - if err != nil { - return ctx, errors.Wrapf( - err, - "failed to create an ethereum core.Message from signer %T", signer, - ) - } - - if baseFee == nil { - return ctx, errors.Wrap( - evm.ErrInvalidBaseFee, - "base fee is supported but evm block context value is nil", - ) - } - if coreMsg.GasFeeCap().Cmp(baseFee) < 0 { - return ctx, errors.Wrapf( - errortypes.ErrInsufficientFee, - "max fee per gas less than block base fee (%s < %s)", - coreMsg.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: gethcommon.Address{}, - BaseFee: baseFee, - } - - stateDB := statedb.New(ctx, &ctd.EvmKeeper, statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes()))) - evm := ctd.EvmKeeper.NewEVM(ctx, coreMsg, cfg, evm.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 coreMsg.Value().Sign() > 0 && !evm.Context.CanTransfer(stateDB, coreMsg.From(), coreMsg.Value()) { - return ctx, errors.Wrapf( - errortypes.ErrInsufficientFunds, - "failed to transfer %s from address %s using the EVM block context transfer function", - coreMsg.Value(), - coreMsg.From(), - ) - } - } - - return next(ctx, tx, simulate) -} - -// AnteDecEthIncrementSenderSequence increments the sequence of the signers. -type AnteDecEthIncrementSenderSequence struct { - AppKeepers -} - -// NewAnteDecEthIncrementSenderSequence creates a new EthIncrementSenderSequenceDecorator. -func NewAnteDecEthIncrementSenderSequence(k AppKeepers) AnteDecEthIncrementSenderSequence { - return AnteDecEthIncrementSenderSequence{ - AppKeepers: k, - } -} - -// AnteHandle handles incrementing the sequence of the signer (i.e. sender). If the transaction is a -// contract creation, the nonce will be incremented during the transaction execution and not within -// this AnteHandler decorator. -func (issd AnteDecEthIncrementSenderSequence) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { - for _, msg := range tx.GetMsgs() { - msgEthTx, ok := msg.(*evm.MsgEthereumTx) - if !ok { - return ctx, errors.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) - } - - txData, err := evm.UnpackTxData(msgEthTx.Data) - if err != nil { - return ctx, errors.Wrap(err, "failed to unpack tx data") - } - - // increase sequence of sender - acc := issd.AccountKeeper.GetAccount(ctx, msgEthTx.GetFrom()) - if acc == nil { - return ctx, errors.Wrapf( - errortypes.ErrUnknownAddress, - "account %s is nil", gethcommon.BytesToAddress(msgEthTx.GetFrom().Bytes()), - ) - } - nonce := acc.GetSequence() - - // we merged the nonce verification to nonce increment, so when tx includes multiple messages - // with same sender, they'll be accepted. - if txData.GetNonce() != nonce { - return ctx, errors.Wrapf( - errortypes.ErrInvalidSequence, - "invalid nonce; got %d, expected %d", txData.GetNonce(), nonce, - ) - } - - if err := acc.SetSequence(nonce + 1); err != nil { - return ctx, errors.Wrapf(err, "failed to set sequence to %d", acc.GetSequence()+1) - } - - issd.AccountKeeper.SetAccount(ctx, acc) - } - - return next(ctx, tx, simulate) -} diff --git a/app/evmante_gas_consume.go b/app/evmante_gas_consume.go new file mode 100644 index 000000000..5406fb827 --- /dev/null +++ b/app/evmante_gas_consume.go @@ -0,0 +1,182 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package app + +import ( + "math" + + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + + gethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/x/evm" + "github.com/NibiruChain/nibiru/x/evm/keeper" +) + +var ( + _ sdk.AnteDecorator = (*AnteDecEthGasConsume)(nil) + _ sdk.AnteDecorator = (*AnteDecVerifyEthAcc)(nil) +) + +// AnteDecEthGasConsume validates enough intrinsic gas for the transaction and +// gas consumption. +type AnteDecEthGasConsume struct { + AppKeepers + // bankKeeper anteutils.BankKeeper + // distributionKeeper anteutils.DistributionKeeper + // evmKeeper EVMKeeper + // stakingKeeper anteutils.StakingKeeper + maxGasWanted uint64 +} + +// NewAnteDecEthGasConsume creates a new EthGasConsumeDecorator +func NewAnteDecEthGasConsume( + keepers AppKeepers, + maxGasWanted uint64, +) AnteDecEthGasConsume { + return AnteDecEthGasConsume{ + AppKeepers: keepers, + maxGasWanted: maxGasWanted, + } +} + +// AnteHandle validates that the Ethereum tx message has enough to cover +// intrinsic gas (during CheckTx only) and that the sender has enough balance to +// pay for the gas cost. If the balance is not sufficient, it will be attempted +// to withdraw enough staking rewards for the payment. +// +// Intrinsic gas for a transaction is the amount of gas that the transaction uses +// before the transaction is executed. The gas is a constant value plus any cost +// incurred by additional bytes of data supplied with the transaction. +// +// This AnteHandler decorator will fail if: +// - the message is not a MsgEthereumTx +// - sender account cannot be found +// - transaction's gas limit is lower than the intrinsic gas +// - user has neither enough balance nor staking rewards to deduct the transaction fees (gas_limit * gas_price) +// - transaction or block gas meter runs out of gas +// - sets the gas meter limit +// - gas limit is greater than the block gas meter limit +func (anteDec AnteDecEthGasConsume) AnteHandle( + ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, +) (sdk.Context, error) { + gasWanted := uint64(0) + if ctx.IsReCheckTx() { + // Then, the limit for gas consumed was already checked during CheckTx so + // there's no need to verify it again during ReCheckTx + // + // Use new context with gasWanted = 0 + // Otherwise, there's an error on txmempool.postCheck (tendermint) + // that is not bubbled up. Thus, the Tx never runs on DeliverMode + // Error: "gas wanted -1 is negative" + newCtx := ctx.WithGasMeter(eth.NewInfiniteGasMeterWithLimit(gasWanted)) + return next(newCtx, tx, simulate) + } + + evmParams := anteDec.EvmKeeper.GetParams(ctx) + evmDenom := evmParams.GetEvmDenom() + + var events sdk.Events + + // Use the lowest priority of all the messages as the final one. + minPriority := int64(math.MaxInt64) + baseFee := anteDec.EvmKeeper.GetBaseFee(ctx) + + for _, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evm.MsgEthereumTx) + if !ok { + return ctx, errors.Wrapf( + errortypes.ErrUnknownRequest, + "invalid message type %T, expected %T", + msg, (*evm.MsgEthereumTx)(nil), + ) + } + from := msgEthTx.GetFrom() + + txData, err := evm.UnpackTxData(msgEthTx.Data) + if err != nil { + return ctx, errors.Wrap(err, "failed to unpack tx data") + } + + if ctx.IsCheckTx() && anteDec.maxGasWanted != 0 { + // We can't trust the tx gas limit, because we'll refund the unused gas. + if txData.GetGas() > anteDec.maxGasWanted { + gasWanted += anteDec.maxGasWanted + } else { + gasWanted += txData.GetGas() + } + } else { + gasWanted += txData.GetGas() + } + + fees, err := keeper.VerifyFee(txData, evmDenom, baseFee, ctx.IsCheckTx()) + if err != nil { + return ctx, errors.Wrapf(err, "failed to verify the fees") + } + + if err = anteDec.deductFee(ctx, fees, from); err != nil { + return ctx, err + } + + events = append(events, + sdk.NewEvent( + sdk.EventTypeTx, + sdk.NewAttribute(sdk.AttributeKeyFee, fees.String()), + ), + ) + + priority := evm.GetTxPriority(txData, baseFee) + + if priority < minPriority { + minPriority = priority + } + } + + ctx.EventManager().EmitEvents(events) + + blockGasLimit := eth.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, errors.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. + newCtx := ctx. + WithGasMeter(eth.NewInfiniteGasMeterWithLimit(gasWanted)). + WithPriority(minPriority) + + // we know that we have enough gas on the pool to cover the intrinsic gas + return next(newCtx, tx, simulate) +} + +// 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 (anteDec AnteDecEthGasConsume) deductFee(ctx sdk.Context, fees sdk.Coins, feePayer sdk.AccAddress) error { + if fees.IsZero() { + return nil + } + + // If the account balance is not sufficient, try to withdraw enough staking rewards + + if err := anteDec.EvmKeeper.DeductTxCostsFromUserBalance(ctx, fees, gethcommon.BytesToAddress(feePayer)); err != nil { + return errors.Wrapf(err, "failed to deduct transaction costs from user balance") + } + return nil +} diff --git a/app/evmante_increment_sender_seq.go b/app/evmante_increment_sender_seq.go new file mode 100644 index 000000000..5a6a76a40 --- /dev/null +++ b/app/evmante_increment_sender_seq.go @@ -0,0 +1,73 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package app + +import ( + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + + gethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/NibiruChain/nibiru/x/evm" +) + +var ( + _ sdk.AnteDecorator = (*AnteDecEthGasConsume)(nil) + _ sdk.AnteDecorator = (*AnteDecVerifyEthAcc)(nil) +) + +// AnteDecEthIncrementSenderSequence increments the sequence of the signers. +type AnteDecEthIncrementSenderSequence struct { + AppKeepers +} + +// NewAnteDecEthIncrementSenderSequence creates a new EthIncrementSenderSequenceDecorator. +func NewAnteDecEthIncrementSenderSequence(k AppKeepers) AnteDecEthIncrementSenderSequence { + return AnteDecEthIncrementSenderSequence{ + AppKeepers: k, + } +} + +// AnteHandle handles incrementing the sequence of the signer (i.e. sender). If the transaction is a +// contract creation, the nonce will be incremented during the transaction execution and not within +// this AnteHandler decorator. +func (issd AnteDecEthIncrementSenderSequence) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + for _, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evm.MsgEthereumTx) + if !ok { + return ctx, errors.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + } + + txData, err := evm.UnpackTxData(msgEthTx.Data) + if err != nil { + return ctx, errors.Wrap(err, "failed to unpack tx data") + } + + // increase sequence of sender + acc := issd.AccountKeeper.GetAccount(ctx, msgEthTx.GetFrom()) + if acc == nil { + return ctx, errors.Wrapf( + errortypes.ErrUnknownAddress, + "account %s is nil", gethcommon.BytesToAddress(msgEthTx.GetFrom().Bytes()), + ) + } + nonce := acc.GetSequence() + + // we merged the nonce verification to nonce increment, so when tx includes multiple messages + // with same sender, they'll be accepted. + if txData.GetNonce() != nonce { + return ctx, errors.Wrapf( + errortypes.ErrInvalidSequence, + "invalid nonce; got %d, expected %d", txData.GetNonce(), nonce, + ) + } + + if err := acc.SetSequence(nonce + 1); err != nil { + return ctx, errors.Wrapf(err, "failed to set sequence to %d", acc.GetSequence()+1) + } + + issd.AccountKeeper.SetAccount(ctx, acc) + } + + return next(ctx, tx, simulate) +} diff --git a/app/evmante_verify_eth_acc.go b/app/evmante_verify_eth_acc.go new file mode 100644 index 000000000..6832bc06e --- /dev/null +++ b/app/evmante_verify_eth_acc.go @@ -0,0 +1,86 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package app + +import ( + "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/NibiruChain/nibiru/x/evm" + "github.com/NibiruChain/nibiru/x/evm/keeper" + "github.com/NibiruChain/nibiru/x/evm/statedb" + + gethcommon "github.com/ethereum/go-ethereum/common" +) + +var ( + _ sdk.AnteDecorator = (*AnteDecEthGasConsume)(nil) + _ sdk.AnteDecorator = (*AnteDecVerifyEthAcc)(nil) +) + +// AnteDecVerifyEthAcc validates an account balance checks +type AnteDecVerifyEthAcc struct { + AppKeepers +} + +// NewAnteDecVerifyEthAcc creates a new EthAccountVerificationDecorator +func NewAnteDecVerifyEthAcc(k AppKeepers) AnteDecVerifyEthAcc { + return AnteDecVerifyEthAcc{ + AppKeepers: k, + } +} + +// AnteHandle validates checks that the sender 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 AnteHandler decorator will fail if: +// - any of the msgs is not a MsgEthereumTx +// - from address is empty +// - account balance is lower than the transaction cost +func (anteDec AnteDecVerifyEthAcc) AnteHandle( + ctx sdk.Context, + tx sdk.Tx, + simulate bool, + next sdk.AnteHandler, +) (newCtx sdk.Context, err error) { + if !ctx.IsCheckTx() { + return next(ctx, tx, simulate) + } + + for i, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evm.MsgEthereumTx) + if !ok { + return ctx, errors.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + } + + txData, err := evm.UnpackTxData(msgEthTx.Data) + if err != nil { + return ctx, errors.Wrapf(err, "failed to unpack tx data any for tx %d", i) + } + + // sender address should be in the tx cache from the previous AnteHandle call + from := msgEthTx.GetFrom() + if from.Empty() { + return ctx, errors.Wrap(errortypes.ErrInvalidAddress, "from address cannot be empty") + } + + // check whether the sender address is EOA + fromAddr := gethcommon.BytesToAddress(from) + acct := anteDec.EvmKeeper.GetAccount(ctx, fromAddr) + + if acct == nil { + acc := anteDec.AccountKeeper.NewAccountWithAddress(ctx, from) + anteDec.AccountKeeper.SetAccount(ctx, acc) + acct = statedb.NewEmptyAccount() + } else if acct.IsContract() { + return ctx, errors.Wrapf(errortypes.ErrInvalidType, + "the sender is not EOA: address %s, codeHash <%s>", fromAddr, acct.CodeHash) + } + + if err := keeper.CheckSenderBalance(sdkmath.NewIntFromBigInt(acct.Balance), txData); err != nil { + return ctx, errors.Wrap(err, "failed to check sender balance") + } + } + return next(ctx, tx, simulate) +} diff --git a/app/evmante_test.go b/app/evmante_verify_eth_acc_test.go similarity index 98% rename from app/evmante_test.go rename to app/evmante_verify_eth_acc_test.go index 7d2ef2140..ef5306116 100644 --- a/app/evmante_test.go +++ b/app/evmante_verify_eth_acc_test.go @@ -189,6 +189,3 @@ func (s *TestSuite) TestAnteDecEthGasConsume() { }) } } - -// func (s *TestSuite) TestAnteDecoratorVerifyEthAcc_CheckTx() { -// } From ec4dfca04e3e48ea44fb34bd9a4cd334b0dfca14 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Tue, 11 Jun 2024 10:53:36 +0200 Subject: [PATCH 11/13] test(evm): evmante tests for can_transfer, fee_checker, gas_comsume --- app/evmante_can_transfer_test.go | 97 ++++++++++++++++++++++++++++ app/evmante_gas_consume_test.go | 106 +++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 app/evmante_can_transfer_test.go create mode 100644 app/evmante_gas_consume_test.go diff --git a/app/evmante_can_transfer_test.go b/app/evmante_can_transfer_test.go new file mode 100644 index 000000000..d4961f011 --- /dev/null +++ b/app/evmante_can_transfer_test.go @@ -0,0 +1,97 @@ +package app_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +func (s *TestSuite) TestNewDynamicFeeChecker() { + testCases := []struct { + name string + txSetup func(deps *evmtest.TestDeps) sdk.FeeTx + ctxSetup func(deps *evmtest.TestDeps) + wantErr string + wantFee int64 + wantPriority int64 + }{ + { + name: "happy: genesis tx with sufficient fee", + ctxSetup: func(deps *evmtest.TestDeps) { + gasPrice := sdk.NewInt64Coin("unibi", 1) + deps.Ctx = deps.Ctx. + WithBlockHeight(0). + WithMinGasPrices( + sdk.NewDecCoins(sdk.NewDecCoinFromCoin(gasPrice)), + ). + WithIsCheckTx(true) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.FeeTx { + txMsg := happyCreateContractTx(deps) + txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + tx, err := txMsg.BuildTx(txBuilder, eth.EthBaseDenom) + s.Require().NoError(err) + return tx + }, + wantErr: "", + wantFee: gasLimitCreateContract().Int64(), + wantPriority: 0, + }, + { + name: "sad: genesis tx insufficient fee", + ctxSetup: func(deps *evmtest.TestDeps) { + gasPrice := sdk.NewInt64Coin("unibi", 2) + deps.Ctx = deps.Ctx. + WithBlockHeight(0). + WithMinGasPrices( + sdk.NewDecCoins(sdk.NewDecCoinFromCoin(gasPrice)), + ). + WithIsCheckTx(true) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.FeeTx { + txMsg := happyCreateContractTx(deps) + txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + tx, err := txMsg.BuildTx(txBuilder, eth.EthBaseDenom) + s.Require().NoError(err) + return tx + }, + wantErr: "insufficient fee", + }, + { + name: "happy: tx with sufficient fee", + txSetup: func(deps *evmtest.TestDeps) sdk.FeeTx { + txMsg := happyCreateContractTx(deps) + txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + tx, err := txMsg.BuildTx(txBuilder, eth.EthBaseDenom) + s.Require().NoError(err) + return tx + }, + wantErr: "", + wantFee: gasLimitCreateContract().Int64(), + wantPriority: 0, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + checker := app.NewDynamicFeeChecker(deps.K) + + if tc.ctxSetup != nil { + tc.ctxSetup(&deps) + } + + fee, priority, err := checker(deps.Ctx, tc.txSetup(&deps)) + + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + s.Require().Equal(tc.wantFee, fee.AmountOf("unibi").Int64()) + s.Require().Equal(tc.wantPriority, priority) + }) + } +} diff --git a/app/evmante_gas_consume_test.go b/app/evmante_gas_consume_test.go new file mode 100644 index 000000000..f77531427 --- /dev/null +++ b/app/evmante_gas_consume_test.go @@ -0,0 +1,106 @@ +package app_test + +import ( + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +func (s *TestSuite) TestGasWantedDecorator() { + testCases := []struct { + name string + ctxSetup func(deps *evmtest.TestDeps) + txSetup func(deps *evmtest.TestDeps) sdk.Tx + wantErr string + }{ + { + name: "happy: non fee tx type", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return happyCreateContractTx(deps) + }, + wantErr: "", + }, + { + name: "happy: tx without gas, block gas limit 1000", + ctxSetup: func(deps *evmtest.TestDeps) { + cp := &tmproto.ConsensusParams{ + Block: &tmproto.BlockParams{MaxGas: 1000}, + } + deps.Ctx = deps.Ctx.WithConsensusParams(cp) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return legacytx.StdTx{ + Msgs: []sdk.Msg{ + happyCreateContractTx(deps), + }, + } + }, + wantErr: "", + }, + { + name: "happy: tx with gas wanted 500, block gas limit 1000", + ctxSetup: func(deps *evmtest.TestDeps) { + cp := &tmproto.ConsensusParams{ + Block: &tmproto.BlockParams{MaxGas: 1000}, + } + deps.Ctx = deps.Ctx.WithConsensusParams(cp) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return legacytx.StdTx{ + Msgs: []sdk.Msg{ + happyCreateContractTx(deps), + }, + Fee: legacytx.StdFee{Gas: 500}, + } + }, + wantErr: "", + }, + { + name: "sad: tx with gas wanted 1000, block gas limit 500", + ctxSetup: func(deps *evmtest.TestDeps) { + cp := &tmproto.ConsensusParams{ + Block: &tmproto.BlockParams{ + MaxGas: 500, + }, + } + deps.Ctx = deps.Ctx.WithConsensusParams(cp) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return legacytx.StdTx{ + Msgs: []sdk.Msg{ + happyCreateContractTx(deps), + }, + Fee: legacytx.StdFee{Gas: 1000}, + } + }, + wantErr: "exceeds block gas limit", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + stateDB := deps.StateDB() + anteDec := app.AnteDecoratorGasWanted{} + + tx := tc.txSetup(&deps) + s.Require().NoError(stateDB.Commit()) + + deps.Ctx = deps.Ctx.WithIsCheckTx(true) + if tc.ctxSetup != nil { + tc.ctxSetup(&deps) + } + _, err := anteDec.AnteHandle( + deps.Ctx, tx, false, NextNoOpAnteHandler, + ) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + }) + } +} From fde23834775159128a9267625d3d9b26690da50d Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Tue, 11 Jun 2024 12:42:46 +0200 Subject: [PATCH 12/13] test(evm): evmante increment_sender_seq, validate_basic, gas_consume, reject_msgs, can_transfer --- app/evmante_can_transfer.go | 17 ++-- app/evmante_can_transfer_test.go | 96 ++++++++++++--------- app/evmante_fee_checker_test.go | 2 +- app/evmante_gas_consume.go | 5 -- app/evmante_gas_consume_test.go | 105 +++++++++-------------- app/evmante_handler_test.go | 104 ++++++++++++++++++++++ app/evmante_increment_sender_seq.go | 17 ++-- app/evmante_increment_sender_seq_test.go | 93 ++++++++++++++++++++ app/evmante_reject_msgs_test.go | 46 ++++++++++ app/evmante_validate_basic_test.go | 2 +- app/evmante_verify_eth_acc_test.go | 88 +++++-------------- 11 files changed, 384 insertions(+), 191 deletions(-) create mode 100644 app/evmante_handler_test.go create mode 100644 app/evmante_increment_sender_seq_test.go create mode 100644 app/evmante_reject_msgs_test.go diff --git a/app/evmante_can_transfer.go b/app/evmante_can_transfer.go index 026a46d26..c2ec4b4a9 100644 --- a/app/evmante_can_transfer.go +++ b/app/evmante_can_transfer.go @@ -40,9 +40,11 @@ func (ctd CanTransferDecorator) AnteHandle( for _, msg := range tx.GetMsgs() { msgEthTx, ok := msg.(*evm.MsgEthereumTx) if !ok { - return ctx, errors.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + return ctx, errors.Wrapf( + errortypes.ErrUnknownRequest, + "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil), + ) } - baseFee := ctd.EvmKeeper.GetBaseFee(ctx) coreMsg, err := msgEthTx.AsMessage(signer, baseFee) @@ -75,12 +77,17 @@ func (ctd CanTransferDecorator) AnteHandle( BaseFee: baseFee, } - stateDB := statedb.New(ctx, &ctd.EvmKeeper, statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes()))) - evm := ctd.EvmKeeper.NewEVM(ctx, coreMsg, cfg, evm.NewNoOpTracer(), stateDB) + stateDB := statedb.New( + ctx, + &ctd.EvmKeeper, + statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes())), + ) + evmInstance := ctd.EvmKeeper.NewEVM(ctx, coreMsg, cfg, evm.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 coreMsg.Value().Sign() > 0 && !evm.Context.CanTransfer(stateDB, coreMsg.From(), coreMsg.Value()) { + if coreMsg.Value().Sign() > 0 && + !evmInstance.Context.CanTransfer(stateDB, coreMsg.From(), coreMsg.Value()) { return ctx, errors.Wrapf( errortypes.ErrInsufficientFunds, "failed to transfer %s from address %s using the EVM block context transfer function", diff --git a/app/evmante_can_transfer_test.go b/app/evmante_can_transfer_test.go index d4961f011..4e70e06a1 100644 --- a/app/evmante_can_transfer_test.go +++ b/app/evmante_can_transfer_test.go @@ -1,97 +1,109 @@ package app_test import ( + "math/big" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/NibiruChain/nibiru/app" "github.com/NibiruChain/nibiru/eth" "github.com/NibiruChain/nibiru/x/evm/evmtest" + "github.com/NibiruChain/nibiru/x/evm/statedb" ) -func (s *TestSuite) TestNewDynamicFeeChecker() { +func (s *TestSuite) TestCanTransferDecorator() { testCases := []struct { - name string - txSetup func(deps *evmtest.TestDeps) sdk.FeeTx - ctxSetup func(deps *evmtest.TestDeps) - wantErr string - wantFee int64 - wantPriority int64 + name string + txSetup func(deps *evmtest.TestDeps) sdk.FeeTx + ctxSetup func(deps *evmtest.TestDeps) + beforeTxSetup func(deps *evmtest.TestDeps, sdb *statedb.StateDB) + wantErr string }{ { - name: "happy: genesis tx with sufficient fee", - ctxSetup: func(deps *evmtest.TestDeps) { - gasPrice := sdk.NewInt64Coin("unibi", 1) - deps.Ctx = deps.Ctx. - WithBlockHeight(0). - WithMinGasPrices( - sdk.NewDecCoins(sdk.NewDecCoinFromCoin(gasPrice)), - ). - WithIsCheckTx(true) + name: "happy: signed tx, sufficient funds", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { + sdb.AddBalance(deps.Sender.EthAddr, big.NewInt(100)) }, txSetup: func(deps *evmtest.TestDeps) sdk.FeeTx { - txMsg := happyCreateContractTx(deps) + txMsg := happyTransfertTx(deps, 0) txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + + gethSigner := deps.Sender.GethSigner(deps.Chain.EvmKeeper.EthChainID(deps.Ctx)) + keyringSigner := deps.Sender.KeyringSigner + err := txMsg.Sign(gethSigner, keyringSigner) + s.Require().NoError(err) + tx, err := txMsg.BuildTx(txBuilder, eth.EthBaseDenom) s.Require().NoError(err) + return tx }, - wantErr: "", - wantFee: gasLimitCreateContract().Int64(), - wantPriority: 0, + wantErr: "", }, { - name: "sad: genesis tx insufficient fee", - ctxSetup: func(deps *evmtest.TestDeps) { - gasPrice := sdk.NewInt64Coin("unibi", 2) - deps.Ctx = deps.Ctx. - WithBlockHeight(0). - WithMinGasPrices( - sdk.NewDecCoins(sdk.NewDecCoinFromCoin(gasPrice)), - ). - WithIsCheckTx(true) - }, + name: "sad: signed tx, insufficient funds", txSetup: func(deps *evmtest.TestDeps) sdk.FeeTx { - txMsg := happyCreateContractTx(deps) + txMsg := happyTransfertTx(deps, 0) txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + + gethSigner := deps.Sender.GethSigner(deps.Chain.EvmKeeper.EthChainID(deps.Ctx)) + keyringSigner := deps.Sender.KeyringSigner + err := txMsg.Sign(gethSigner, keyringSigner) + s.Require().NoError(err) + tx, err := txMsg.BuildTx(txBuilder, eth.EthBaseDenom) s.Require().NoError(err) + return tx }, - wantErr: "insufficient fee", + wantErr: "insufficient funds", }, { - name: "happy: tx with sufficient fee", + name: "sad: unsigned tx", txSetup: func(deps *evmtest.TestDeps) sdk.FeeTx { - txMsg := happyCreateContractTx(deps) + txMsg := happyTransfertTx(deps, 0) txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + tx, err := txMsg.BuildTx(txBuilder, eth.EthBaseDenom) s.Require().NoError(err) + return tx }, - wantErr: "", - wantFee: gasLimitCreateContract().Int64(), - wantPriority: 0, + wantErr: "invalid transaction", + }, + { + name: "sad: tx with non evm message", + txSetup: func(deps *evmtest.TestDeps) sdk.FeeTx { + return nonEvmMsgTx(deps).(sdk.FeeTx) + }, + wantErr: "invalid message", }, } for _, tc := range testCases { s.Run(tc.name, func() { deps := evmtest.NewTestDeps() - checker := app.NewDynamicFeeChecker(deps.K) + stateDB := deps.StateDB() + anteDec := app.NewCanTransferDecorator(deps.Chain.AppKeepers) + tx := tc.txSetup(&deps) if tc.ctxSetup != nil { tc.ctxSetup(&deps) } + if tc.beforeTxSetup != nil { + tc.beforeTxSetup(&deps, stateDB) + err := stateDB.Commit() + s.Require().NoError(err) + } - fee, priority, err := checker(deps.Ctx, tc.txSetup(&deps)) - + _, err := anteDec.AnteHandle( + deps.Ctx, tx, false, NextNoOpAnteHandler, + ) if tc.wantErr != "" { s.Require().ErrorContains(err, tc.wantErr) return } s.Require().NoError(err) - s.Require().Equal(tc.wantFee, fee.AmountOf("unibi").Int64()) - s.Require().Equal(tc.wantPriority, priority) }) } } diff --git a/app/evmante_fee_checker_test.go b/app/evmante_fee_checker_test.go index d4961f011..184045704 100644 --- a/app/evmante_fee_checker_test.go +++ b/app/evmante_fee_checker_test.go @@ -8,7 +8,7 @@ import ( "github.com/NibiruChain/nibiru/x/evm/evmtest" ) -func (s *TestSuite) TestNewDynamicFeeChecker() { +func (s *TestSuite) TestDynamicFeeChecker() { testCases := []struct { name string txSetup func(deps *evmtest.TestDeps) sdk.FeeTx diff --git a/app/evmante_gas_consume.go b/app/evmante_gas_consume.go index 5406fb827..b50613e99 100644 --- a/app/evmante_gas_consume.go +++ b/app/evmante_gas_consume.go @@ -15,11 +15,6 @@ import ( "github.com/NibiruChain/nibiru/x/evm/keeper" ) -var ( - _ sdk.AnteDecorator = (*AnteDecEthGasConsume)(nil) - _ sdk.AnteDecorator = (*AnteDecVerifyEthAcc)(nil) -) - // AnteDecEthGasConsume validates enough intrinsic gas for the transaction and // gas consumption. type AnteDecEthGasConsume struct { diff --git a/app/evmante_gas_consume_test.go b/app/evmante_gas_consume_test.go index f77531427..8918a20a9 100644 --- a/app/evmante_gas_consume_test.go +++ b/app/evmante_gas_consume_test.go @@ -1,82 +1,58 @@ package app_test import ( - tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + "math/big" + sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/x/evm" "github.com/NibiruChain/nibiru/x/evm/evmtest" + "github.com/NibiruChain/nibiru/x/evm/statedb" ) -func (s *TestSuite) TestGasWantedDecorator() { +func (s *TestSuite) TestAnteDecEthGasConsume() { testCases := []struct { - name string - ctxSetup func(deps *evmtest.TestDeps) - txSetup func(deps *evmtest.TestDeps) sdk.Tx - wantErr string + name string + beforeTxSetup func(deps *evmtest.TestDeps, sdb *statedb.StateDB) + txSetup func(deps *evmtest.TestDeps) *evm.MsgEthereumTx + wantErr string + maxGasWanted uint64 + gasMeter sdk.GasMeter }{ { - name: "happy: non fee tx type", - txSetup: func(deps *evmtest.TestDeps) sdk.Tx { - return happyCreateContractTx(deps) + name: "happy: sender with funds", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { + gasLimit := happyGasLimit() + balance := new(big.Int).Add(gasLimit, big.NewInt(100)) + sdb.AddBalance(deps.Sender.EthAddr, balance) }, - wantErr: "", + txSetup: happyCreateContractTx, + wantErr: "", + gasMeter: eth.NewInfiniteGasMeterWithLimit(happyGasLimit().Uint64()), + maxGasWanted: 0, }, { - name: "happy: tx without gas, block gas limit 1000", - ctxSetup: func(deps *evmtest.TestDeps) { - cp := &tmproto.ConsensusParams{ - Block: &tmproto.BlockParams{MaxGas: 1000}, - } - deps.Ctx = deps.Ctx.WithConsensusParams(cp) - }, - txSetup: func(deps *evmtest.TestDeps) sdk.Tx { - return legacytx.StdTx{ - Msgs: []sdk.Msg{ - happyCreateContractTx(deps), - }, - } + name: "happy: is recheck tx", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { + deps.Ctx = deps.Ctx.WithIsReCheckTx(true) }, - wantErr: "", + txSetup: happyCreateContractTx, + gasMeter: eth.NewInfiniteGasMeterWithLimit(0), + wantErr: "", }, { - name: "happy: tx with gas wanted 500, block gas limit 1000", - ctxSetup: func(deps *evmtest.TestDeps) { - cp := &tmproto.ConsensusParams{ - Block: &tmproto.BlockParams{MaxGas: 1000}, - } - deps.Ctx = deps.Ctx.WithConsensusParams(cp) - }, - txSetup: func(deps *evmtest.TestDeps) sdk.Tx { - return legacytx.StdTx{ - Msgs: []sdk.Msg{ - happyCreateContractTx(deps), - }, - Fee: legacytx.StdFee{Gas: 500}, - } + name: "sad: out of gas", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { + gasLimit := happyGasLimit() + balance := new(big.Int).Add(gasLimit, big.NewInt(100)) + sdb.AddBalance(deps.Sender.EthAddr, balance) }, - wantErr: "", - }, - { - name: "sad: tx with gas wanted 1000, block gas limit 500", - ctxSetup: func(deps *evmtest.TestDeps) { - cp := &tmproto.ConsensusParams{ - Block: &tmproto.BlockParams{ - MaxGas: 500, - }, - } - deps.Ctx = deps.Ctx.WithConsensusParams(cp) - }, - txSetup: func(deps *evmtest.TestDeps) sdk.Tx { - return legacytx.StdTx{ - Msgs: []sdk.Msg{ - happyCreateContractTx(deps), - }, - Fee: legacytx.StdFee{Gas: 1000}, - } - }, - wantErr: "exceeds block gas limit", + txSetup: happyCreateContractTx, + wantErr: "exceeds block gas limit (0)", + gasMeter: eth.NewInfiniteGasMeterWithLimit(0), + maxGasWanted: 0, }, } @@ -84,15 +60,16 @@ func (s *TestSuite) TestGasWantedDecorator() { s.Run(tc.name, func() { deps := evmtest.NewTestDeps() stateDB := deps.StateDB() - anteDec := app.AnteDecoratorGasWanted{} + anteDec := app.NewAnteDecEthGasConsume( + deps.Chain.AppKeepers, tc.maxGasWanted, + ) + tc.beforeTxSetup(&deps, stateDB) tx := tc.txSetup(&deps) s.Require().NoError(stateDB.Commit()) deps.Ctx = deps.Ctx.WithIsCheckTx(true) - if tc.ctxSetup != nil { - tc.ctxSetup(&deps) - } + deps.Ctx = deps.Ctx.WithBlockGasMeter(tc.gasMeter) _, err := anteDec.AnteHandle( deps.Ctx, tx, false, NextNoOpAnteHandler, ) diff --git a/app/evmante_handler_test.go b/app/evmante_handler_test.go new file mode 100644 index 000000000..e60b075b1 --- /dev/null +++ b/app/evmante_handler_test.go @@ -0,0 +1,104 @@ +package app_test + +import ( + "math/big" + + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/app/ante" + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/x/evm/evmtest" + "github.com/NibiruChain/nibiru/x/evm/statedb" +) + +func (s *TestSuite) TestAnteHandlerEVM() { + testCases := []struct { + name string + txSetup func(deps *evmtest.TestDeps) sdk.FeeTx + ctxSetup func(deps *evmtest.TestDeps) + beforeTxSetup func(deps *evmtest.TestDeps, sdb *statedb.StateDB) + wantErr string + }{ + { + name: "happy: signed tx, sufficient funds", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { + sdb.AddBalance( + deps.Sender.EthAddr, + new(big.Int).Add(gasLimitCreateContract(), big.NewInt(100)), + ) + }, + ctxSetup: func(deps *evmtest.TestDeps) { + gasPrice := sdk.NewInt64Coin("unibi", 1) + cp := &tmproto.ConsensusParams{ + Block: &tmproto.BlockParams{ + MaxGas: new(big.Int).Add(gasLimitCreateContract(), big.NewInt(100)).Int64(), + }, + } + deps.Ctx = deps.Ctx. + WithMinGasPrices( + sdk.NewDecCoins(sdk.NewDecCoinFromCoin(gasPrice)), + ). + WithIsCheckTx(true). + WithConsensusParams(cp) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.FeeTx { + txMsg := happyTransfertTx(deps, 0) + txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + + gethSigner := deps.Sender.GethSigner(deps.Chain.EvmKeeper.EthChainID(deps.Ctx)) + keyringSigner := deps.Sender.KeyringSigner + err := txMsg.Sign(gethSigner, keyringSigner) + s.Require().NoError(err) + + tx, err := txMsg.BuildTx(txBuilder, eth.EthBaseDenom) + s.Require().NoError(err) + + return tx + }, + wantErr: "", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + stateDB := deps.StateDB() + + anteHandlerEVM := app.NewAnteHandlerEVM( + deps.Chain.AppKeepers, ante.AnteHandlerOptions{ + HandlerOptions: authante.HandlerOptions{ + AccountKeeper: deps.Chain.AccountKeeper, + BankKeeper: deps.Chain.BankKeeper, + FeegrantKeeper: deps.Chain.FeeGrantKeeper, + SignModeHandler: deps.EncCfg.TxConfig.SignModeHandler(), + SigGasConsumer: authante.DefaultSigVerificationGasConsumer, + ExtensionOptionChecker: func(*codectypes.Any) bool { return true }, + }, + }) + + tx := tc.txSetup(&deps) + + if tc.ctxSetup != nil { + tc.ctxSetup(&deps) + } + if tc.beforeTxSetup != nil { + tc.beforeTxSetup(&deps, stateDB) + err := stateDB.Commit() + s.Require().NoError(err) + } + + _, err := anteHandlerEVM( + deps.Ctx, tx, false, + ) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + }) + } +} diff --git a/app/evmante_increment_sender_seq.go b/app/evmante_increment_sender_seq.go index 5a6a76a40..ef4ac75a5 100644 --- a/app/evmante_increment_sender_seq.go +++ b/app/evmante_increment_sender_seq.go @@ -11,11 +11,6 @@ import ( "github.com/NibiruChain/nibiru/x/evm" ) -var ( - _ sdk.AnteDecorator = (*AnteDecEthGasConsume)(nil) - _ sdk.AnteDecorator = (*AnteDecVerifyEthAcc)(nil) -) - // AnteDecEthIncrementSenderSequence increments the sequence of the signers. type AnteDecEthIncrementSenderSequence struct { AppKeepers @@ -31,11 +26,19 @@ func NewAnteDecEthIncrementSenderSequence(k AppKeepers) AnteDecEthIncrementSende // AnteHandle handles incrementing the sequence of the signer (i.e. sender). If the transaction is a // contract creation, the nonce will be incremented during the transaction execution and not within // this AnteHandler decorator. -func (issd AnteDecEthIncrementSenderSequence) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { +func (issd AnteDecEthIncrementSenderSequence) AnteHandle( + ctx sdk.Context, + tx sdk.Tx, + simulate bool, + next sdk.AnteHandler, +) (sdk.Context, error) { for _, msg := range tx.GetMsgs() { msgEthTx, ok := msg.(*evm.MsgEthereumTx) if !ok { - return ctx, errors.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + return ctx, errors.Wrapf( + errortypes.ErrUnknownRequest, + "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil), + ) } txData, err := evm.UnpackTxData(msgEthTx.Data) diff --git a/app/evmante_increment_sender_seq_test.go b/app/evmante_increment_sender_seq_test.go new file mode 100644 index 000000000..7778522e0 --- /dev/null +++ b/app/evmante_increment_sender_seq_test.go @@ -0,0 +1,93 @@ +package app_test + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/x/evm/evmtest" + "github.com/NibiruChain/nibiru/x/evm/statedb" +) + +func (s *TestSuite) TestAnteDecEthIncrementSenderSequence() { + testCases := []struct { + name string + beforeTxSetup func(deps *evmtest.TestDeps, sdb *statedb.StateDB) + txSetup func(deps *evmtest.TestDeps) sdk.Tx + wantErr string + wantSeq uint64 + }{ + { + name: "happy: single message", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { + balance := big.NewInt(100) + sdb.AddBalance(deps.Sender.EthAddr, balance) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return happyTransfertTx(deps, 0) + }, + wantErr: "", + wantSeq: 1, + }, + { + name: "happy: two messages", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { + balance := big.NewInt(100) + sdb.AddBalance(deps.Sender.EthAddr, balance) + }, + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + txMsgOne := happyTransfertTx(deps, 0) + txMsgTwo := happyTransfertTx(deps, 1) + + txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + s.Require().NoError(txBuilder.SetMsgs(txMsgOne, txMsgTwo)) + + tx := txBuilder.GetTx() + return tx + }, + wantErr: "", + wantSeq: 2, + }, + { + name: "sad: account does not exists", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return happyTransfertTx(deps, 0) + }, + wantErr: "unknown address", + }, + { + name: "sad: tx with non evm message", + txSetup: nonEvmMsgTx, + wantErr: "invalid message", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + stateDB := deps.StateDB() + anteDec := app.NewAnteDecEthIncrementSenderSequence(deps.Chain.AppKeepers) + + if tc.beforeTxSetup != nil { + tc.beforeTxSetup(&deps, stateDB) + s.Require().NoError(stateDB.Commit()) + } + tx := tc.txSetup(&deps) + + _, err := anteDec.AnteHandle( + deps.Ctx, tx, false, NextNoOpAnteHandler, + ) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + + if tc.wantSeq > 0 { + seq := deps.Chain.AccountKeeper.GetAccount(deps.Ctx, deps.Sender.NibiruAddr).GetSequence() + s.Require().Equal(tc.wantSeq, seq) + } + }) + } +} diff --git a/app/evmante_reject_msgs_test.go b/app/evmante_reject_msgs_test.go new file mode 100644 index 000000000..76636340f --- /dev/null +++ b/app/evmante_reject_msgs_test.go @@ -0,0 +1,46 @@ +package app_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +func (s *TestSuite) TestAnteDecoratorPreventEtheruemTxMsgs() { + testCases := []struct { + name string + txSetup func(deps *evmtest.TestDeps) sdk.Tx + wantErr string + }{ + { + name: "sad: evm message", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return happyTransfertTx(deps, 0) + }, + wantErr: "invalid type", + }, + { + name: "happy: non evm message", + txSetup: nonEvmMsgTx, + wantErr: "", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + anteDec := app.AnteDecoratorPreventEtheruemTxMsgs{} + tx := tc.txSetup(&deps) + + _, err := anteDec.AnteHandle( + deps.Ctx, tx, false, NextNoOpAnteHandler, + ) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + }) + } +} diff --git a/app/evmante_validate_basic_test.go b/app/evmante_validate_basic_test.go index ae11eb68c..934a69580 100644 --- a/app/evmante_validate_basic_test.go +++ b/app/evmante_validate_basic_test.go @@ -269,7 +269,7 @@ func buildTx( msg sdk.Msg, gasLimit uint64, fees sdk.Coins, -) sdk.Tx { +) sdk.FeeTx { txBuilder, _ := deps.EncCfg.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder) if ethExtentions { option, _ := codectypes.NewAnyWithValue(&evm.ExtensionOptionsEthereumTx{}) diff --git a/app/evmante_verify_eth_acc_test.go b/app/evmante_verify_eth_acc_test.go index ef5306116..c8f124ac0 100644 --- a/app/evmante_verify_eth_acc_test.go +++ b/app/evmante_verify_eth_acc_test.go @@ -4,11 +4,11 @@ import ( "math/big" sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" gethparams "github.com/ethereum/go-ethereum/params" "github.com/NibiruChain/nibiru/app" - "github.com/NibiruChain/nibiru/eth" "github.com/NibiruChain/nibiru/x/evm" "github.com/NibiruChain/nibiru/x/evm/evmtest" @@ -120,72 +120,28 @@ func happyCreateContractTx(deps *evmtest.TestDeps) *evm.MsgEthereumTx { return tx } -func (s *TestSuite) TestAnteDecEthGasConsume() { - testCases := []struct { - name string - beforeTxSetup func(deps *evmtest.TestDeps, sdb *statedb.StateDB) - txSetup func(deps *evmtest.TestDeps) *evm.MsgEthereumTx - wantErr string - maxGasWanted uint64 - gasMeter sdk.GasMeter - }{ - { - name: "happy: sender with funds", - beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { - gasLimit := happyGasLimit() - balance := new(big.Int).Add(gasLimit, big.NewInt(100)) - sdb.AddBalance(deps.Sender.EthAddr, balance) - }, - txSetup: happyCreateContractTx, - wantErr: "", - gasMeter: eth.NewInfiniteGasMeterWithLimit(happyGasLimit().Uint64()), - maxGasWanted: 0, - }, - { - name: "happy: is recheck tx", - beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { - deps.Ctx = deps.Ctx.WithIsReCheckTx(true) - }, - txSetup: happyCreateContractTx, - gasMeter: eth.NewInfiniteGasMeterWithLimit(0), - wantErr: "", - }, - { - name: "sad: out of gas", - beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { - gasLimit := happyGasLimit() - balance := new(big.Int).Add(gasLimit, big.NewInt(100)) - sdb.AddBalance(deps.Sender.EthAddr, balance) - }, - txSetup: happyCreateContractTx, - wantErr: "exceeds block gas limit (0)", - gasMeter: eth.NewInfiniteGasMeterWithLimit(0), - maxGasWanted: 0, - }, +func happyTransfertTx(deps *evmtest.TestDeps, nonce uint64) *evm.MsgEthereumTx { + to := evmtest.NewEthAccInfo().EthAddr + ethContractCreationTxParams := &evm.EvmTxArgs{ + ChainID: deps.Chain.EvmKeeper.EthChainID(deps.Ctx), + Nonce: nonce, + Amount: big.NewInt(10), + GasLimit: gasLimitCreateContract().Uint64(), + GasPrice: big.NewInt(1), + To: &to, } + tx := evm.NewTx(ethContractCreationTxParams) + tx.From = deps.Sender.EthAddr.Hex() + return tx +} - for _, tc := range testCases { - s.Run(tc.name, func() { - deps := evmtest.NewTestDeps() - stateDB := deps.StateDB() - anteDec := app.NewAnteDecEthGasConsume( - deps.Chain.AppKeepers, tc.maxGasWanted, - ) - - tc.beforeTxSetup(&deps, stateDB) - tx := tc.txSetup(&deps) - s.Require().NoError(stateDB.Commit()) - - deps.Ctx = deps.Ctx.WithIsCheckTx(true) - deps.Ctx = deps.Ctx.WithBlockGasMeter(tc.gasMeter) - _, err := anteDec.AnteHandle( - deps.Ctx, tx, false, NextNoOpAnteHandler, - ) - if tc.wantErr != "" { - s.Require().ErrorContains(err, tc.wantErr) - return - } - s.Require().NoError(err) - }) +func nonEvmMsgTx(deps *evmtest.TestDeps) sdk.Tx { + gasLimit := uint64(10) + fees := sdk.NewCoins(sdk.NewInt64Coin("unibi", int64(gasLimit))) + msg := &banktypes.MsgSend{ + FromAddress: deps.Sender.NibiruAddr.String(), + ToAddress: evmtest.NewEthAccInfo().NibiruAddr.String(), + Amount: sdk.NewCoins(sdk.NewInt64Coin("unibi", 1)), } + return buildTx(deps, true, msg, gasLimit, fees) } From 8a50e0e2a35e94b0319ed2a622a7b01465410898 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Tue, 11 Jun 2024 13:08:03 +0200 Subject: [PATCH 13/13] test: tried -v flag for integration tests --- contrib/make/test.mk | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/make/test.mk b/contrib/make/test.mk index 766d98dd5..84e839a4e 100644 --- a/contrib/make/test.mk +++ b/contrib/make/test.mk @@ -20,7 +20,8 @@ test-coverage-integration: go test ./... \ -coverprofile=coverage.txt \ -covermode=atomic \ - -race + -race \ + -v # Require Python3 .PHONY: test-create-test-cases