From 6511f86214c1c8a1d624797294e013a52079cf0d Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Tue, 11 Jun 2024 18:48:56 +0400 Subject: [PATCH] test(evm): unit tests for evm_ante (#1912) * test(evm): unit tests for evm_ante_sigverify * chore: changelog update * test(evm): ante gas wanted full coverage * test(evm): ante handler setup ctx coverage * test(evm): ante handler emit event coverage * test(evm): evmante hanlers emit_event, setup_ctx and validate_basic * fix: lint * test(evm): evmante fee checker coverage * test(evm): evmante fees test coverage * test(evm): split evmante handlers 1 handler 1 file * test(evm): evmante tests for can_transfer, fee_checker, gas_comsume * test(evm): evmante increment_sender_seq, validate_basic, gas_consume, reject_msgs, can_transfer * test: tried -v flag for integration tests --- CHANGELOG.md | 3 +- app/evmante_can_transfer.go | 101 +++++ app/evmante_can_transfer_test.go | 109 +++++ app/evmante_emit_event.go | 59 +++ app/evmante_emit_event_test.go | 79 ++++ app/evmante_eth.go | 386 ------------------ app/evmante_fee_checker.go | 28 +- app/evmante_fee_checker_test.go | 97 +++++ app/evmante_fees.go | 4 +- app/evmante_fees_test.go | 103 +++++ app/evmante_gas_consume.go | 177 ++++++++ app/evmante_gas_consume_test.go | 83 ++++ ...te_fee_market.go => evmante_gas_wanted.go} | 0 app/evmante_gas_wanted_test.go | 106 +++++ app/evmante_handler_test.go | 104 +++++ app/evmante_increment_sender_seq.go | 76 ++++ app/evmante_increment_sender_seq_test.go | 93 +++++ app/evmante_reject_msgs_test.go | 46 +++ app/evmante_setup_ctx.go | 159 +------- app/evmante_setup_ctx_test.go | 43 ++ app/evmante_sigverify.go | 9 +- app/evmante_sigverify_test.go | 88 ++++ app/evmante_validate_basic.go | 169 ++++++++ app/evmante_validate_basic_test.go | 286 +++++++++++++ app/evmante_verify_eth_acc.go | 86 ++++ ...test.go => evmante_verify_eth_acc_test.go} | 91 +---- contrib/make/test.mk | 3 +- x/evm/tx.go | 1 + 28 files changed, 1969 insertions(+), 620 deletions(-) create mode 100644 app/evmante_can_transfer.go create mode 100644 app/evmante_can_transfer_test.go create mode 100644 app/evmante_emit_event.go create mode 100644 app/evmante_emit_event_test.go delete mode 100644 app/evmante_eth.go create mode 100644 app/evmante_fee_checker_test.go create mode 100644 app/evmante_fees_test.go create mode 100644 app/evmante_gas_consume.go create mode 100644 app/evmante_gas_consume_test.go rename app/{evmante_fee_market.go => evmante_gas_wanted.go} (100%) create mode 100644 app/evmante_gas_wanted_test.go create mode 100644 app/evmante_handler_test.go create mode 100644 app/evmante_increment_sender_seq.go create mode 100644 app/evmante_increment_sender_seq_test.go create mode 100644 app/evmante_reject_msgs_test.go create mode 100644 app/evmante_setup_ctx_test.go create mode 100644 app/evmante_sigverify_test.go create mode 100644 app/evmante_validate_basic.go create mode 100644 app/evmante_validate_basic_test.go create mode 100644 app/evmante_verify_eth_acc.go rename app/{evmante_test.go => evmante_verify_eth_acc_test.go} (60%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 825b34f15..7f87a4cb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,7 +70,8 @@ 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 -- [#1914](https://github.com/NibiruChain/nibiru/pull/1914) - refactor(evm): Remove dead code and document non-EVM ante handler +- [#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. #### Dapp modules: perp, spot, oracle, etc diff --git a/app/evmante_can_transfer.go b/app/evmante_can_transfer.go new file mode 100644 index 000000000..c2ec4b4a9 --- /dev/null +++ b/app/evmante_can_transfer.go @@ -0,0 +1,101 @@ +// 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())), + ) + 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 && + !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", + coreMsg.Value(), + coreMsg.From(), + ) + } + } + + return next(ctx, tx, simulate) +} diff --git a/app/evmante_can_transfer_test.go b/app/evmante_can_transfer_test.go new file mode 100644 index 000000000..4e70e06a1 --- /dev/null +++ b/app/evmante_can_transfer_test.go @@ -0,0 +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) TestCanTransferDecorator() { + 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, big.NewInt(100)) + }, + 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: "", + }, + { + name: "sad: signed tx, insufficient funds", + 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: "insufficient funds", + }, + { + name: "sad: unsigned tx", + txSetup: func(deps *evmtest.TestDeps) sdk.FeeTx { + txMsg := happyTransfertTx(deps, 0) + txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + + tx, err := txMsg.BuildTx(txBuilder, eth.EthBaseDenom) + s.Require().NoError(err) + + return tx + }, + 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() + 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) + } + + _, 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_emit_event.go b/app/evmante_emit_event.go new file mode 100644 index 000000000..b621a8dbe --- /dev/null +++ b/app/evmante_emit_event.go @@ -0,0 +1,59 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package app + +import ( + "strconv" + + errorsmod "cosmossdk.io/errors" + 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). +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_emit_event_test.go b/app/evmante_emit_event_test.go new file mode 100644 index 000000000..39aa1351f --- /dev/null +++ b/app/evmante_emit_event_test.go @@ -0,0 +1,79 @@ +package app_test + +import ( + 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" +) + +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) + }) + } +} 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_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..184045704 --- /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) TestDynamicFeeChecker() { + 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_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) + }) + } +} diff --git a/app/evmante_gas_consume.go b/app/evmante_gas_consume.go new file mode 100644 index 000000000..b50613e99 --- /dev/null +++ b/app/evmante_gas_consume.go @@ -0,0 +1,177 @@ +// 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" +) + +// 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_gas_consume_test.go b/app/evmante_gas_consume_test.go new file mode 100644 index 000000000..8918a20a9 --- /dev/null +++ b/app/evmante_gas_consume_test.go @@ -0,0 +1,83 @@ +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" + "github.com/NibiruChain/nibiru/x/evm/evmtest" + "github.com/NibiruChain/nibiru/x/evm/statedb" +) + +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, + }, + } + + 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) + }) + } +} 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..f77531427 --- /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.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) + }) + } +} 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 new file mode 100644 index 000000000..ef4ac75a5 --- /dev/null +++ b/app/evmante_increment_sender_seq.go @@ -0,0 +1,76 @@ +// 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" +) + +// 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_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_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..132ed8f1a --- /dev/null +++ b/app/evmante_setup_ctx_test.go @@ -0,0 +1,43 @@ +package app_test + +import ( + "math" + + 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() { + 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_sigverify.go b/app/evmante_sigverify.go index 45817a8d2..314681cd8 100644 --- a/app/evmante_sigverify.go +++ b/app/evmante_sigverify.go @@ -41,7 +41,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() @@ -49,7 +52,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) + }) + } +} diff --git a/app/evmante_validate_basic.go b/app/evmante_validate_basic.go new file mode 100644 index 000000000..2c006485a --- /dev/null +++ b/app/evmante_validate_basic.go @@ -0,0 +1,169 @@ +// 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) +} diff --git a/app/evmante_validate_basic_test.go b/app/evmante_validate_basic_test.go new file mode 100644 index 000000000..934a69580 --- /dev/null +++ b/app/evmante_validate_basic_test.go @@ -0,0 +1,286 @@ +package app_test + +import ( + "math/big" + + 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() { + testCases := []struct { + name string + ctxSetup func(deps *evmtest.TestDeps) + txSetup func(deps *evmtest.TestDeps) sdk.Tx + paramsSetup func(deps *evmtest.TestDeps) evm.Params + wantErr string + }{ + { + name: "happy: properly built eth tx", + 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: "", + }, + { + 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: fail chain id basic validation", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + return happyCreateContractTx(deps) + }, + wantErr: "invalid chain-id", + }, + { + 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: "sad: eth tx with memo should fail", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + txBuilder.SetMemo("memo") + tx, err := happyCreateContractTx(deps).BuildTx(txBuilder, eth.EthBaseDenom) + s.Require().NoError(err) + return tx + }, + wantErr: "invalid request", + }, + { + name: "sad: eth tx with fee payer should fail", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + txBuilder.SetFeePayer(testutil.AccAddress()) + tx, err := happyCreateContractTx(deps).BuildTx(txBuilder, eth.EthBaseDenom) + s.Require().NoError(err) + return tx + }, + wantErr: "invalid request", + }, + { + name: "sad: eth tx with fee granter should fail", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + txBuilder.SetFeeGranter(testutil.AccAddress()) + tx, err := happyCreateContractTx(deps).BuildTx(txBuilder, eth.EthBaseDenom) + s.Require().NoError(err) + return tx + }, + wantErr: "invalid request", + }, + { + name: "sad: eth tx with signatures should fail", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + txBuilder := deps.EncCfg.TxConfig.NewTxBuilder() + sigV2 := signing.SignatureV2{ + PubKey: deps.Sender.PrivKey.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: deps.EncCfg.TxConfig.SignModeHandler().DefaultMode(), + Signature: nil, + }, + Sequence: 0, + } + err := txBuilder.SetSignatures(sigV2) + s.Require().NoError(err) + txMsg := happyCreateContractTx(deps) + + 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: "tx AuthInfo SignerInfos should be empty", + }, + { + name: "sad: tx for contract creation with param disabled", + paramsSetup: func(deps *evmtest.TestDeps) evm.Params { + params := evm.DefaultParams() + params.EnableCreate = false + return params + }, + 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: "EVM Create operation is disabled", + }, + { + name: "sad: tx for contract call with param disabled", + paramsSetup: func(deps *evmtest.TestDeps) evm.Params { + params := evm.DefaultParams() + params.EnableCall = false + return params + }, + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + chainID := deps.Chain.EvmKeeper.EthChainID(deps.Ctx) + gasLimit := uint64(10) + to := evmtest.NewEthAccInfo().EthAddr + fees := sdk.NewCoins(sdk.NewInt64Coin("unibi", int64(gasLimit))) + msg := buildEthMsg(chainID, gasLimit, "", &to) + return buildTx(deps, true, msg, gasLimit, fees) + }, + wantErr: "EVM Call operation is disabled", + }, + { + name: "sad: tx without extension options should fail", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + chainID := deps.Chain.EvmKeeper.EthChainID(deps.Ctx) + gasLimit := uint64(10) + fees := sdk.NewCoins(sdk.NewInt64Coin("unibi", int64(gasLimit))) + msg := buildEthMsg(chainID, gasLimit, deps.Sender.NibiruAddr.String(), nil) + return buildTx(deps, false, msg, gasLimit, fees) + }, + wantErr: "for eth tx length of ExtensionOptions should be 1", + }, + { + name: "sad: tx with non evm message", + 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", + }, + { + name: "sad: tx with from value set should fail", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + chainID := deps.Chain.EvmKeeper.EthChainID(deps.Ctx) + gasLimit := uint64(10) + fees := sdk.NewCoins(sdk.NewInt64Coin("unibi", int64(gasLimit))) + msg := buildEthMsg(chainID, gasLimit, deps.Sender.NibiruAddr.String(), nil) + return buildTx(deps, true, msg, gasLimit, fees) + }, + wantErr: "invalid From", + }, + { + name: "sad: tx with fee <> msg fee", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + chainID := deps.Chain.EvmKeeper.EthChainID(deps.Ctx) + gasLimit := uint64(10) + fees := sdk.NewCoins(sdk.NewInt64Coin("unibi", 5)) + msg := buildEthMsg(chainID, gasLimit, "", nil) + return buildTx(deps, true, msg, gasLimit, fees) + }, + wantErr: "invalid AuthInfo Fee Amount", + }, + { + name: "sad: tx with gas limit <> msg gas limit", + txSetup: func(deps *evmtest.TestDeps) sdk.Tx { + chainID := deps.Chain.EvmKeeper.EthChainID(deps.Ctx) + gasLimit := uint64(10) + fees := sdk.NewCoins(sdk.NewInt64Coin("unibi", int64(gasLimit))) + msg := buildEthMsg(chainID, gasLimit, "", nil) + return buildTx(deps, true, msg, 5, fees) + }, + wantErr: "invalid AuthInfo Fee GasLimit", + }, + } + + 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) + } + if tc.paramsSetup != nil { + deps.K.SetParams(deps.Ctx, tc.paramsSetup(&deps)) + } + _, err := anteDec.AnteHandle( + deps.Ctx, tx, false, NextNoOpAnteHandler, + ) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + }) + } +} + +func buildEthMsg( + chainID *big.Int, + gasLimit uint64, + from string, + to *common.Address, + +) *evm.MsgEthereumTx { + ethContractCreationTxParams := &evm.EvmTxArgs{ + ChainID: chainID, + Nonce: 1, + Amount: big.NewInt(10), + GasLimit: gasLimit, + GasPrice: big.NewInt(1), + To: to, + } + tx := evm.NewTx(ethContractCreationTxParams) + tx.From = from + return tx +} + +func buildTx( + deps *evmtest.TestDeps, + ethExtentions bool, + msg sdk.Msg, + gasLimit uint64, + fees sdk.Coins, +) sdk.FeeTx { + txBuilder, _ := deps.EncCfg.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder) + if ethExtentions { + option, _ := codectypes.NewAnyWithValue(&evm.ExtensionOptionsEthereumTx{}) + txBuilder.SetExtensionOptions(option) + } + err := txBuilder.SetMsgs(msg) + if err != nil { + panic(err) + } + txBuilder.SetGasLimit(gasLimit) + txBuilder.SetFeeAmount(fees) + + return txBuilder.GetTx() +} 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 60% rename from app/evmante_test.go rename to app/evmante_verify_eth_acc_test.go index 7d2ef2140..c8f124ac0 100644 --- a/app/evmante_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,75 +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) } - -// func (s *TestSuite) TestAnteDecoratorVerifyEthAcc_CheckTx() { -// } 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 diff --git a/x/evm/tx.go b/x/evm/tx.go index 44e0fecd8..2abff89e2 100644 --- a/x/evm/tx.go +++ b/x/evm/tx.go @@ -20,6 +20,7 @@ import ( // EvmTxArgs encapsulates all possible params to create all EVM txs types. // This includes LegacyTx, DynamicFeeTx and AccessListTx + type EvmTxArgs struct { //revive:disable-line:exported Nonce uint64 GasLimit uint64