From 89cc59188e89a496f98ac19691bad49234c32e50 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Mon, 10 Jun 2024 18:57:57 +0200 Subject: [PATCH] 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() { -// }