From 87355c8bd07309baa552fc1ef699d8bc184c2391 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Sat, 26 Oct 2024 00:08:14 +0400 Subject: [PATCH 1/5] feat(evm): gas usage in precompiles: limits, local gas meters --- x/evm/keeper/erc20.go | 177 ++--------- x/evm/keeper/funtoken_from_coin.go | 76 ++++- x/evm/keeper/funtoken_from_coin_test.go | 2 + x/evm/keeper/funtoken_from_erc20.go | 93 ++++++ x/evm/keeper/funtoken_from_erc20_test.go | 4 + x/evm/keeper/grpc_query.go | 8 +- x/evm/keeper/keeper.go | 9 +- x/evm/keeper/msg_server.go | 356 +++++++++++------------ x/evm/precompile/funtoken.go | 22 ++ x/evm/precompile/funtoken_test.go | 4 +- x/evm/precompile/oracle.go | 18 +- x/evm/precompile/oracle_test.go | 7 +- x/evm/precompile/precompile.go | 45 ++- x/evm/precompile/test/export.go | 21 +- x/evm/precompile/wasm.go | 17 ++ x/evm/precompile/wasm_test.go | 28 +- x/evm/statedb/journal_test.go | 9 +- 17 files changed, 530 insertions(+), 366 deletions(-) diff --git a/x/evm/keeper/erc20.go b/x/evm/keeper/erc20.go index 10404bea4..0838a7412 100644 --- a/x/evm/keeper/erc20.go +++ b/x/evm/keeper/erc20.go @@ -5,19 +5,26 @@ import ( "fmt" "math/big" - "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" gethabi "github.com/ethereum/go-ethereum/accounts/abi" gethcommon "github.com/ethereum/go-ethereum/common" - gethcore "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" - - serverconfig "github.com/NibiruChain/nibiru/v2/app/server/config" "github.com/NibiruChain/nibiru/v2/x/evm" "github.com/NibiruChain/nibiru/v2/x/evm/embeds" ) +const ( + // Erc20GasLimitDeploy only used internally when deploying ERC20Minter. + // Deployment requires ~1_600_000 gas + Erc20GasLimitDeploy uint64 = 2_000_000 + // Erc20GasLimitQuery used only for querying name, symbol and decimals + // Cannot be heavy. Only if the contract is malicious. + Erc20GasLimitQuery uint64 = 100_000 + // Erc20GasLimitExecute used for transfer, mint and burn. + // All must not exceed 200_000 + Erc20GasLimitExecute uint64 = 200_000 +) + // ERC20 returns a mutable reference to the keeper with an ERC20 contract ABI and // Go functions corresponding to contract calls in the ERC20 standard like "mint" // and "transfer" in the ERC20 standard. @@ -56,7 +63,7 @@ func (e erc20Calls) Mint( if err != nil { return nil, fmt.Errorf("failed to pack ABI args: %w", err) } - evmResp, _, err = e.CallContractWithInput(ctx, from, &contract, true, input) + evmResp, _, err = e.CallContractWithInput(ctx, from, &contract, true, input, Erc20GasLimitExecute) return evmResp, err } @@ -78,7 +85,7 @@ func (e erc20Calls) Transfer( if err != nil { return false, fmt.Errorf("failed to pack ABI args: %w", err) } - resp, _, err := e.CallContractWithInput(ctx, from, &contract, true, input) + resp, _, err := e.CallContractWithInput(ctx, from, &contract, true, input, Erc20GasLimitExecute) if err != nil { return false, err } @@ -118,152 +125,10 @@ func (e erc20Calls) Burn( return } commit := true - evmResp, _, err = e.CallContractWithInput(ctx, from, &contract, commit, input) + evmResp, _, err = e.CallContractWithInput(ctx, from, &contract, commit, input, Erc20GasLimitExecute) return } -// CallContract invokes a smart contract on the method specified by [methodName] -// using the given [args]. -// -// Parameters: -// - ctx: The SDK context for the transaction. -// - abi: The ABI (Application Binary Interface) of the smart contract. -// - fromAcc: The Ethereum address of the account initiating the contract call. -// - contract: Pointer to the Ethereum address of the contract to be called. -// - commit: Boolean flag indicating whether to commit the transaction (true) or simulate it (false). -// - methodName: The name of the contract method to be called. -// - args: Variadic parameter for the arguments to be passed to the contract method. -// -// Note: This function handles both contract method calls and simulations, -// depending on the 'commit' parameter. It uses a default gas limit for -// simulations and estimates gas for actual transactions. -func (k Keeper) CallContract( - ctx sdk.Context, - abi *gethabi.ABI, - fromAcc gethcommon.Address, - contract *gethcommon.Address, - commit bool, - methodName string, - args ...any, -) (evmResp *evm.MsgEthereumTxResponse, err error) { - contractInput, err := abi.Pack(methodName, args...) - if err != nil { - return nil, fmt.Errorf("failed to pack ABI args: %w", err) - } - evmResp, _, err = k.CallContractWithInput(ctx, fromAcc, contract, commit, contractInput) - return evmResp, err -} - -// CallContractWithInput invokes a smart contract with the given [contractInput] -// or deploys a new contract. -// -// Parameters: -// - ctx: The SDK context for the transaction. -// - fromAcc: The Ethereum address of the account initiating the contract call. -// - contract: Pointer to the Ethereum address of the contract. Nil if new -// contract is deployed. -// - commit: Boolean flag indicating whether to commit the transaction (true) -// or simulate it (false). -// - contractInput: Hexadecimal-encoded bytes use as input data to the call. -// -// Note: This function handles both contract method calls and simulations, -// depending on the 'commit' parameter. It uses a default gas limit. -func (k Keeper) CallContractWithInput( - ctx sdk.Context, - fromAcc gethcommon.Address, - contract *gethcommon.Address, - commit bool, - contractInput []byte, -) (evmResp *evm.MsgEthereumTxResponse, evmObj *vm.EVM, err error) { - // This is a `defer` pattern to add behavior that runs in the case that the - // error is non-nil, creating a concise way to add extra information. - defer func() { - if err != nil { - err = fmt.Errorf("CallContractError: %w", err) - } - }() - nonce := k.GetAccNonce(ctx, fromAcc) - - // Gas cap sufficient for all "honest" ERC20 calls without malicious (gas - // intensive) code in contracts - gasLimit := serverconfig.DefaultEthCallGasLimit - - unusedBigInt := big.NewInt(0) - evmMsg := gethcore.NewMessage( - fromAcc, - contract, - nonce, - unusedBigInt, // amount - gasLimit, - unusedBigInt, // gasFeeCap - unusedBigInt, // gasTipCap - unusedBigInt, // gasPrice - contractInput, - gethcore.AccessList{}, - !commit, // isFake - ) - - // Apply EVM message - evmCfg, err := k.GetEVMConfig( - ctx, - sdk.ConsAddress(ctx.BlockHeader().ProposerAddress), - k.EthChainID(ctx), - ) - if err != nil { - err = errors.Wrapf(err, "failed to load EVM config") - return - } - - // Generating TxConfig with an empty tx hash as there is no actual eth tx - // sent by a user - txConfig := k.TxConfig(ctx, gethcommon.BigToHash(big.NewInt(0))) - - // Using tmp context to not modify the state in case of evm revert - tmpCtx, commitCtx := ctx.CacheContext() - - evmResp, evmObj, err = k.ApplyEvmMsg( - tmpCtx, evmMsg, evm.NewNoOpTracer(), commit, evmCfg, txConfig, - ) - if err != nil { - // We don't know the actual gas used, so consuming the gas limit - k.ResetGasMeterAndConsumeGas(ctx, gasLimit) - err = errors.Wrap(err, "failed to apply ethereum core message") - return - } - if evmResp.Failed() { - k.ResetGasMeterAndConsumeGas(ctx, evmResp.GasUsed) - if evmResp.VmError != vm.ErrOutOfGas.Error() { - if evmResp.VmError == vm.ErrExecutionReverted.Error() { - err = fmt.Errorf("VMError: %w", evm.NewExecErrorWithReason(evmResp.Ret)) - return - } - err = fmt.Errorf("VMError: %s", evmResp.VmError) - return - } - err = fmt.Errorf("gas required exceeds allowance (%d)", gasLimit) - return - } else { - // Success, committing the state to ctx - if commit { - commitCtx() - totalGasUsed, err := k.AddToBlockGasUsed(ctx, evmResp.GasUsed) - if err != nil { - k.ResetGasMeterAndConsumeGas(ctx, ctx.GasMeter().Limit()) - return nil, nil, errors.Wrap(err, "error adding transient gas used to block") - } - k.ResetGasMeterAndConsumeGas(ctx, totalGasUsed) - k.updateBlockBloom(ctx, evmResp, uint64(txConfig.LogIndex)) - err = k.EmitEthereumTxEvents(ctx, contract, gethcore.LegacyTxType, evmMsg, evmResp) - if err != nil { - return nil, nil, errors.Wrap(err, "error emitting ethereum tx events") - } - blockTxIdx := uint64(txConfig.TxIndex) + 1 - k.EvmState.BlockTxIndex.Set(ctx, blockTxIdx) - } - return evmResp, evmObj, nil - } -} - func (k Keeper) LoadERC20Name( ctx sdk.Context, abi *gethabi.ABI, erc20 gethcommon.Address, ) (out string, err error) { @@ -289,10 +154,13 @@ func (k Keeper) LoadERC20String( methodName string, ) (out string, err error) { res, err := k.CallContract( - ctx, erc20Abi, + ctx, + erc20Abi, evm.EVM_MODULE_ADDRESS, &erc20Contract, - false, methodName, + false, + Erc20GasLimitQuery, + methodName, ) if err != nil { return out, err @@ -318,7 +186,9 @@ func (k Keeper) loadERC20Uint8( ctx, erc20Abi, evm.EVM_MODULE_ADDRESS, &erc20Contract, - false, methodName, + false, + Erc20GasLimitQuery, + methodName, ) if err != nil { return out, err @@ -347,6 +217,7 @@ func (k Keeper) LoadERC20BigInt( evm.EVM_MODULE_ADDRESS, &contract, false, + Erc20GasLimitQuery, methodName, args..., ) diff --git a/x/evm/keeper/funtoken_from_coin.go b/x/evm/keeper/funtoken_from_coin.go index 6f0f2efd0..f2110dc98 100644 --- a/x/evm/keeper/funtoken_from_coin.go +++ b/x/evm/keeper/funtoken_from_coin.go @@ -1,6 +1,7 @@ package keeper import ( + "context" "fmt" "cosmossdk.io/errors" @@ -79,7 +80,7 @@ func (k *Keeper) deployERC20ForBankCoin( // nil address for contract creation _, _, err = k.CallContractWithInput( - ctx, evm.EVM_MODULE_ADDRESS, nil, true, bytecodeForCall, + ctx, evm.EVM_MODULE_ADDRESS, nil, true, bytecodeForCall, Erc20GasLimitDeploy, ) if err != nil { return gethcommon.Address{}, errors.Wrap(err, "failed to deploy ERC20 contract") @@ -87,3 +88,76 @@ func (k *Keeper) deployERC20ForBankCoin( return erc20Addr, nil } + +// ConvertCoinToEvm Sends a coin with a valid "FunToken" mapping to the +// given recipient address ("to_eth_addr") in the corresponding ERC20 +// representation. +func (k *Keeper) ConvertCoinToEvm( + goCtx context.Context, msg *evm.MsgConvertCoinToEvm, +) (resp *evm.MsgConvertCoinToEvmResponse, err error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + sender := sdk.MustAccAddressFromBech32(msg.Sender) + + funTokens := k.FunTokens.Collect(ctx, k.FunTokens.Indexes.BankDenom.ExactMatch(ctx, msg.BankCoin.Denom)) + if len(funTokens) == 0 { + return nil, fmt.Errorf("funtoken for bank denom \"%s\" does not exist", msg.BankCoin.Denom) + } + if len(funTokens) > 1 { + return nil, fmt.Errorf("multiple funtokens for bank denom \"%s\" found", msg.BankCoin.Denom) + } + + fungibleTokenMapping := funTokens[0] + + if fungibleTokenMapping.IsMadeFromCoin { + return k.convertCoinNativeCoin(ctx, sender, msg.ToEthAddr.Address, msg.BankCoin, fungibleTokenMapping) + } else { + return k.convertCoinNativeERC20(ctx, sender, msg.ToEthAddr.Address, msg.BankCoin, fungibleTokenMapping) + } +} + +// Converts a native coin to an ERC20 token. +// EVM module owns the ERC-20 contract and can mint the ERC-20 tokens. +func (k Keeper) convertCoinNativeCoin( + ctx sdk.Context, + sender sdk.AccAddress, + recipient gethcommon.Address, + coin sdk.Coin, + funTokenMapping evm.FunToken, +) (*evm.MsgConvertCoinToEvmResponse, error) { + // Step 1: Escrow bank coins with EVM module account + err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, evm.ModuleName, sdk.NewCoins(coin)) + if err != nil { + return nil, errors.Wrap(err, "failed to send coins to module account") + } + + erc20Addr := funTokenMapping.Erc20Addr.Address + + // Step 2: mint ERC-20 tokens for recipient + evmResp, err := k.CallContract( + ctx, + embeds.SmartContract_ERC20Minter.ABI, + evm.EVM_MODULE_ADDRESS, + &erc20Addr, + true, + Erc20GasLimitExecute, + "mint", + recipient, + coin.Amount.BigInt(), + ) + if err != nil { + return nil, err + } + if evmResp.Failed() { + return nil, + fmt.Errorf("failed to mint erc-20 tokens of contract %s", erc20Addr.String()) + } + _ = ctx.EventManager().EmitTypedEvent(&evm.EventConvertCoinToEvm{ + Sender: sender.String(), + Erc20ContractAddress: erc20Addr.String(), + ToEthAddr: recipient.String(), + BankCoin: coin, + }) + + return &evm.MsgConvertCoinToEvmResponse{}, nil +} diff --git a/x/evm/keeper/funtoken_from_coin_test.go b/x/evm/keeper/funtoken_from_coin_test.go index 1f5e3a85a..1f65efe7e 100644 --- a/x/evm/keeper/funtoken_from_coin_test.go +++ b/x/evm/keeper/funtoken_from_coin_test.go @@ -264,6 +264,7 @@ func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() { alice.EthAddr, &precompile.PrecompileAddr_FunToken, true, + precompile.FunTokenGasLimitBankSend, "bankSend", funTokenErc20Addr.Address, big.NewInt(10), @@ -291,6 +292,7 @@ func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() { alice.EthAddr, &precompile.PrecompileAddr_FunToken, true, + precompile.FunTokenGasLimitBankSend, "bankSend", funTokenErc20Addr.Address, big.NewInt(10), diff --git a/x/evm/keeper/funtoken_from_erc20.go b/x/evm/keeper/funtoken_from_erc20.go index e10f0cca8..997d986e3 100644 --- a/x/evm/keeper/funtoken_from_erc20.go +++ b/x/evm/keeper/funtoken_from_erc20.go @@ -5,6 +5,7 @@ import ( "fmt" "math/big" + "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" bank "github.com/cosmos/cosmos-sdk/x/bank/types" gethcommon "github.com/ethereum/go-ethereum/common" @@ -202,3 +203,95 @@ func (erc20Info ERC20Metadata) ToBankMetadata( Symbol: symbol, } } + +// Converts a coin that was originally an ERC20 token, and that was converted to a bank coin, back to an ERC20 token. +// EVM module does not own the ERC-20 contract and cannot mint the ERC-20 tokens. +// EVM module has escrowed tokens in the first conversion from ERC-20 to bank coin. +func (k Keeper) convertCoinNativeERC20( + ctx sdk.Context, + sender sdk.AccAddress, + recipient gethcommon.Address, + coin sdk.Coin, + funTokenMapping evm.FunToken, +) (*evm.MsgConvertCoinToEvmResponse, error) { + erc20Addr := funTokenMapping.Erc20Addr.Address + + recipientBalanceBefore, err := k.ERC20().BalanceOf(erc20Addr, recipient, ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve balance") + } + if recipientBalanceBefore == nil { + return nil, fmt.Errorf("failed to retrieve balance, balance is nil") + } + + // Escrow Coins on module account + if err := k.bankKeeper.SendCoinsFromAccountToModule( + ctx, + sender, + evm.ModuleName, + sdk.NewCoins(coin), + ); err != nil { + return nil, errors.Wrap(err, "failed to escrow coins") + } + + // verify that the EVM module account has enough escrowed ERC-20 to transfer + // should never fail, because the coins were minted from the escrowed tokens, but check just in case + evmModuleBalance, err := k.ERC20().BalanceOf( + erc20Addr, + evm.EVM_MODULE_ADDRESS, + ctx, + ) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve balance") + } + if evmModuleBalance == nil { + return nil, fmt.Errorf("failed to retrieve balance, balance is nil") + } + if evmModuleBalance.Cmp(coin.Amount.BigInt()) < 0 { + return nil, fmt.Errorf("insufficient balance in EVM module account") + } + + // unescrow ERC-20 tokens from EVM module address + res, err := k.ERC20().Transfer( + erc20Addr, + evm.EVM_MODULE_ADDRESS, + recipient, + coin.Amount.BigInt(), + ctx, + ) + if err != nil { + return nil, errors.Wrap(err, "failed to transfer ERC20 tokens") + } + if !res { + return nil, fmt.Errorf("failed to transfer ERC20 tokens") + } + + // Check expected Receiver balance after transfer execution + recipientBalanceAfter, err := k.ERC20().BalanceOf(erc20Addr, recipient, ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve balance") + } + if recipientBalanceAfter == nil { + return nil, fmt.Errorf("failed to retrieve balance, balance is nil") + } + + expectedFinalBalance := big.NewInt(0).Add(recipientBalanceBefore, coin.Amount.BigInt()) + if r := recipientBalanceAfter.Cmp(expectedFinalBalance); r != 0 { + return nil, fmt.Errorf("expected balance after transfer to be %s, got %s", expectedFinalBalance, recipientBalanceAfter) + } + + // Burn escrowed Coins + err = k.bankKeeper.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(coin)) + if err != nil { + return nil, errors.Wrap(err, "failed to burn coins") + } + + _ = ctx.EventManager().EmitTypedEvent(&evm.EventConvertCoinToEvm{ + Sender: sender.String(), + Erc20ContractAddress: funTokenMapping.Erc20Addr.String(), + ToEthAddr: recipient.String(), + BankCoin: coin, + }) + + return &evm.MsgConvertCoinToEvmResponse{}, nil +} diff --git a/x/evm/keeper/funtoken_from_erc20_test.go b/x/evm/keeper/funtoken_from_erc20_test.go index eb209f7ca..1d1659fad 100644 --- a/x/evm/keeper/funtoken_from_erc20_test.go +++ b/x/evm/keeper/funtoken_from_erc20_test.go @@ -202,6 +202,7 @@ func (s *FunTokenFromErc20Suite) TestSendFromEvmToBank() { deps.Sender.EthAddr, &deployResp.ContractAddr, true, + keeper.Erc20GasLimitExecute, "mint", deps.Sender.EthAddr, big.NewInt(69_420), @@ -217,6 +218,7 @@ func (s *FunTokenFromErc20Suite) TestSendFromEvmToBank() { deps.Sender.EthAddr, &precompile.PrecompileAddr_FunToken, true, + precompile.FunTokenGasLimitBankSend, "bankSend", deployResp.ContractAddr, big.NewInt(1), @@ -238,6 +240,7 @@ func (s *FunTokenFromErc20Suite) TestSendFromEvmToBank() { deps.Sender.EthAddr, &precompile.PrecompileAddr_FunToken, true, + precompile.FunTokenGasLimitBankSend, "bankSend", deployResp.ContractAddr, big.NewInt(70_000), @@ -365,6 +368,7 @@ func (s *FunTokenFromErc20Suite) TestFunTokenFromERC20MaliciousTransfer() { deps.Sender.EthAddr, &precompile.PrecompileAddr_FunToken, true, + precompile.FunTokenGasLimitBankSend, "bankSend", deployResp.ContractAddr, big.NewInt(1), diff --git a/x/evm/keeper/grpc_query.go b/x/evm/keeper/grpc_query.go index 9cc9290e9..7d4f99c06 100644 --- a/x/evm/keeper/grpc_query.go +++ b/x/evm/keeper/grpc_query.go @@ -284,7 +284,7 @@ func (k *Keeper) EthCall( txConfig := statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash())) // pass false to not commit StateDB - res, _, err := k.ApplyEvmMsg(ctx, msg, nil, false, cfg, txConfig) + res, _, err := k.ApplyEvmMsg(ctx, msg, nil, false, cfg, txConfig, false) if err != nil { return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) } @@ -422,7 +422,7 @@ func (k Keeper) EstimateGasForEvmCallType( WithTransientKVGasConfig(storetypes.GasConfig{}) } // pass false to not commit StateDB - rsp, _, err = k.ApplyEvmMsg(tmpCtx, msg, nil, false, cfg, txConfig) + rsp, _, err = k.ApplyEvmMsg(tmpCtx, msg, nil, false, cfg, txConfig, false) if err != nil { if errors.Is(err, core.ErrIntrinsicGas) { return true, nil, nil // Special case, raise gas limit @@ -518,7 +518,7 @@ func (k Keeper) TraceTx( ctx = ctx.WithGasMeter(eth.NewInfiniteGasMeterWithLimit(msg.Gas())). WithKVGasConfig(storetypes.GasConfig{}). WithTransientKVGasConfig(storetypes.GasConfig{}) - rsp, _, err := k.ApplyEvmMsg(ctx, msg, evm.NewNoOpTracer(), true, cfg, txConfig) + rsp, _, err := k.ApplyEvmMsg(ctx, msg, evm.NewNoOpTracer(), true, cfg, txConfig, false) if err != nil { continue } @@ -800,7 +800,7 @@ func (k *Keeper) TraceEthTxMsg( ctx = ctx.WithGasMeter(eth.NewInfiniteGasMeterWithLimit(msg.Gas())). WithKVGasConfig(storetypes.GasConfig{}). WithTransientKVGasConfig(storetypes.GasConfig{}) - res, _, err := k.ApplyEvmMsg(ctx, msg, tracer, commitMessage, cfg, txConfig) + res, _, err := k.ApplyEvmMsg(ctx, msg, tracer, commitMessage, cfg, txConfig, false) if err != nil { return nil, 0, grpcstatus.Error(grpccodes.Internal, err.Error()) } diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index 49ea0c9bf..e5ece3d4e 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -4,12 +4,12 @@ package keeper import ( "math/big" + "cosmossdk.io/math" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/vm" gethparams "github.com/ethereum/go-ethereum/params" sdkerrors "cosmossdk.io/errors" - "cosmossdk.io/math" "github.com/cometbft/cometbft/libs/log" "github.com/cosmos/cosmos-sdk/codec" @@ -109,8 +109,11 @@ func (k *Keeper) AddToBlockGasUsed( return result, nil } -// GetMinGasMultiplier returns minimum gas multiplier. -func (k Keeper) GetMinGasMultiplier(ctx sdk.Context) math.LegacyDec { +// GetMinGasUsedMultiplier - value from 0 to 1 +// When executing evm msg, user specifies gasLimit. +// If the gasLimit is X times higher than the actual gasUsed then +// we update gasUsed = max(gasUsed, gasLimit * minGasUsedPct) +func (k Keeper) GetMinGasUsedMultiplier(ctx sdk.Context) math.LegacyDec { return math.LegacyNewDecWithPrec(50, 2) // 50% } diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 89a249dd3..2fbf2909b 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -13,6 +13,7 @@ import ( tmbytes "github.com/cometbft/cometbft/libs/bytes" tmtypes "github.com/cometbft/cometbft/types" sdk "github.com/cosmos/cosmos-sdk/types" + gethabi "github.com/ethereum/go-ethereum/accounts/abi" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" gethcore "github.com/ethereum/go-ethereum/core/types" @@ -22,7 +23,6 @@ import ( "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/x/evm" - "github.com/NibiruChain/nibiru/v2/x/evm/embeds" "github.com/NibiruChain/nibiru/v2/x/evm/statedb" ) @@ -62,7 +62,7 @@ func (k *Keeper) EthereumTx( tmpCtx, commitCtx := ctx.CacheContext() // pass true to commit the StateDB - evmResp, _, err = k.ApplyEvmMsg(tmpCtx, evmMsg, nil, true, evmConfig, txConfig) + evmResp, _, err = k.ApplyEvmMsg(tmpCtx, evmMsg, nil, true, evmConfig, txConfig, false) if err != nil { // when a transaction contains multiple msg, as long as one of the msg fails // all gas will be deducted. so is not msg.Gas() @@ -234,18 +234,21 @@ func (k Keeper) GetHashFn(ctx sdk.Context) vm.GetHashFunc { // 1. set up the initial access list // // # Tracer parameter -// // It should be a `vm.Tracer` object or nil, if pass `nil`, it'll create a default one based on keeper options. // // # Commit parameter -// // If commit is true, the `StateDB` will be committed, otherwise discarded. +// +// # fullRefundLeftoverGas parameter +// For internal calls like funtokens, user does not specify gas limit explicitly. +// In this case we don't apply any caps for refund and refund 100% func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, msg core.Message, tracer vm.EVMLogger, commit bool, evmConfig *statedb.EVMConfig, txConfig statedb.TxConfig, + fullRefundLeftoverGas bool, ) (resp *evm.MsgEthereumTxResponse, evmObj *vm.EVM, err error) { var ( ret []byte // return bytes from evm execution @@ -268,7 +271,6 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, sender := vm.AccountRef(msg.From()) contractCreation := msg.To() == nil - intrinsicGas, err := k.GetEthIntrinsicGas(ctx, msg, evmConfig.ChainConfig, contractCreation) if err != nil { // should have already been checked on Ante Handler @@ -328,21 +330,6 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, ) } - // After EIP-3529: refunds are capped to gasUsed / 5 - refundQuotient := params.RefundQuotientEIP3529 - - // calculate gas refund - if msg.Gas() < leftoverGas { - return nil, evmObj, errors.Wrap(evm.ErrGasOverflow, "apply message") - } - // refund gas - temporaryGasUsed := msg.Gas() - leftoverGas - refund := GasToRefund(stateDB.GetRefund(), temporaryGasUsed, refundQuotient) - - // update leftoverGas and temporaryGasUsed with refund amount - leftoverGas += refund - temporaryGasUsed -= refund - // EVM execution error needs to be available for the JSON-RPC client var vmError string if vmErr != nil { @@ -355,19 +342,35 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, return nil, evmObj, fmt.Errorf("failed to commit stateDB: %w", err) } } + // Rare case of uint64 gas overflow + if msg.Gas() < leftoverGas { + return nil, evmObj, errors.Wrap(evm.ErrGasOverflow, "apply message") + } - gasLimit := math.LegacyNewDec(int64(msg.Gas())) - minGasMultiplier := k.GetMinGasMultiplier(ctx) - minimumGasUsed := gasLimit.Mul(minGasMultiplier) + // GAS REFUND + // If msg.Gas() > gasUsed, we need to refund extra gas. + // leftoverGas = amount of extra (not used) gas. + // If the msg comes from user, we apply refundQuotient capping the refund to 20% of used gas + // If msg is internal (funtoken), we refund 100% - if !minimumGasUsed.TruncateInt().IsUint64() { - return nil, evmObj, errors.Wrapf(evm.ErrGasOverflow, "minimumGasUsed(%s) is not a uint64", minimumGasUsed.TruncateInt().String()) + refundQuotient := params.RefundQuotientEIP3529 // EIP-3529: refunds are capped to gasUsed / 5 + minGasUsedPct := k.GetMinGasUsedMultiplier(ctx) // Evmos invention: https://github.com/evmos/ethermint/issues/1085 + if fullRefundLeftoverGas { + refundQuotient = 1 // 100% refund + minGasUsedPct = math.LegacyZeroDec() // no minimum, get the actual gasUsed value } + temporaryGasUsed := msg.Gas() - leftoverGas + refund := GasToRefund(stateDB.GetRefund(), temporaryGasUsed, refundQuotient) + // update leftoverGas and temporaryGasUsed with refund amount + leftoverGas += refund + temporaryGasUsed -= refund if msg.Gas() < leftoverGas { return nil, evmObj, errors.Wrapf(evm.ErrGasOverflow, "message gas limit < leftover gas (%d < %d)", msg.Gas(), leftoverGas) } + // Min gas used is a % of gasLimit + minimumGasUsed := math.LegacyNewDec(int64(msg.Gas())).Mul(minGasUsedPct) gasUsed := math.LegacyMaxDec(minimumGasUsed, math.LegacyNewDec(int64(temporaryGasUsed))).TruncateInt().Uint64() // This resulting "leftoverGas" is used by the tracer. This happens as a @@ -470,170 +473,6 @@ func (k Keeper) FeeForCreateFunToken(ctx sdk.Context) sdk.Coins { return sdk.NewCoins(sdk.NewCoin(evm.EVMBankDenom, evmParams.CreateFuntokenFee)) } -// ConvertCoinToEvm Sends a coin with a valid "FunToken" mapping to the -// given recipient address ("to_eth_addr") in the corresponding ERC20 -// representation. -func (k *Keeper) ConvertCoinToEvm( - goCtx context.Context, msg *evm.MsgConvertCoinToEvm, -) (resp *evm.MsgConvertCoinToEvmResponse, err error) { - ctx := sdk.UnwrapSDKContext(goCtx) - - sender := sdk.MustAccAddressFromBech32(msg.Sender) - - funTokens := k.FunTokens.Collect(ctx, k.FunTokens.Indexes.BankDenom.ExactMatch(ctx, msg.BankCoin.Denom)) - if len(funTokens) == 0 { - return nil, fmt.Errorf("funtoken for bank denom \"%s\" does not exist", msg.BankCoin.Denom) - } - if len(funTokens) > 1 { - return nil, fmt.Errorf("multiple funtokens for bank denom \"%s\" found", msg.BankCoin.Denom) - } - - fungibleTokenMapping := funTokens[0] - - if fungibleTokenMapping.IsMadeFromCoin { - return k.convertCoinNativeCoin(ctx, sender, msg.ToEthAddr.Address, msg.BankCoin, fungibleTokenMapping) - } else { - return k.convertCoinNativeERC20(ctx, sender, msg.ToEthAddr.Address, msg.BankCoin, fungibleTokenMapping) - } -} - -// Converts a native coin to an ERC20 token. -// EVM module owns the ERC-20 contract and can mint the ERC-20 tokens. -func (k Keeper) convertCoinNativeCoin( - ctx sdk.Context, - sender sdk.AccAddress, - recipient gethcommon.Address, - coin sdk.Coin, - funTokenMapping evm.FunToken, -) (*evm.MsgConvertCoinToEvmResponse, error) { - // Step 1: Escrow bank coins with EVM module account - err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, evm.ModuleName, sdk.NewCoins(coin)) - if err != nil { - return nil, errors.Wrap(err, "failed to send coins to module account") - } - - erc20Addr := funTokenMapping.Erc20Addr.Address - - // Step 2: mint ERC-20 tokens for recipient - evmResp, err := k.CallContract( - ctx, - embeds.SmartContract_ERC20Minter.ABI, - evm.EVM_MODULE_ADDRESS, - &erc20Addr, - true, - "mint", - recipient, - coin.Amount.BigInt(), - ) - if err != nil { - return nil, err - } - if evmResp.Failed() { - return nil, - fmt.Errorf("failed to mint erc-20 tokens of contract %s", erc20Addr.String()) - } - _ = ctx.EventManager().EmitTypedEvent(&evm.EventConvertCoinToEvm{ - Sender: sender.String(), - Erc20ContractAddress: erc20Addr.String(), - ToEthAddr: recipient.String(), - BankCoin: coin, - }) - - return &evm.MsgConvertCoinToEvmResponse{}, nil -} - -// Converts a coin that was originally an ERC20 token, and that was converted to a bank coin, back to an ERC20 token. -// EVM module does not own the ERC-20 contract and cannot mint the ERC-20 tokens. -// EVM module has escrowed tokens in the first conversion from ERC-20 to bank coin. -func (k Keeper) convertCoinNativeERC20( - ctx sdk.Context, - sender sdk.AccAddress, - recipient gethcommon.Address, - coin sdk.Coin, - funTokenMapping evm.FunToken, -) (*evm.MsgConvertCoinToEvmResponse, error) { - erc20Addr := funTokenMapping.Erc20Addr.Address - - recipientBalanceBefore, err := k.ERC20().BalanceOf(erc20Addr, recipient, ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to retrieve balance") - } - if recipientBalanceBefore == nil { - return nil, fmt.Errorf("failed to retrieve balance, balance is nil") - } - - // Escrow Coins on module account - if err := k.bankKeeper.SendCoinsFromAccountToModule( - ctx, - sender, - evm.ModuleName, - sdk.NewCoins(coin), - ); err != nil { - return nil, errors.Wrap(err, "failed to escrow coins") - } - - // verify that the EVM module account has enough escrowed ERC-20 to transfer - // should never fail, because the coins were minted from the escrowed tokens, but check just in case - evmModuleBalance, err := k.ERC20().BalanceOf( - erc20Addr, - evm.EVM_MODULE_ADDRESS, - ctx, - ) - if err != nil { - return nil, errors.Wrap(err, "failed to retrieve balance") - } - if evmModuleBalance == nil { - return nil, fmt.Errorf("failed to retrieve balance, balance is nil") - } - if evmModuleBalance.Cmp(coin.Amount.BigInt()) < 0 { - return nil, fmt.Errorf("insufficient balance in EVM module account") - } - - // unescrow ERC-20 tokens from EVM module address - res, err := k.ERC20().Transfer( - erc20Addr, - evm.EVM_MODULE_ADDRESS, - recipient, - coin.Amount.BigInt(), - ctx, - ) - if err != nil { - return nil, errors.Wrap(err, "failed to transfer ERC20 tokens") - } - if !res { - return nil, fmt.Errorf("failed to transfer ERC20 tokens") - } - - // Check expected Receiver balance after transfer execution - recipientBalanceAfter, err := k.ERC20().BalanceOf(erc20Addr, recipient, ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to retrieve balance") - } - if recipientBalanceAfter == nil { - return nil, fmt.Errorf("failed to retrieve balance, balance is nil") - } - - expectedFinalBalance := big.NewInt(0).Add(recipientBalanceBefore, coin.Amount.BigInt()) - if r := recipientBalanceAfter.Cmp(expectedFinalBalance); r != 0 { - return nil, fmt.Errorf("expected balance after transfer to be %s, got %s", expectedFinalBalance, recipientBalanceAfter) - } - - // Burn escrowed Coins - err = k.bankKeeper.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(coin)) - if err != nil { - return nil, errors.Wrap(err, "failed to burn coins") - } - - _ = ctx.EventManager().EmitTypedEvent(&evm.EventConvertCoinToEvm{ - Sender: sender.String(), - Erc20ContractAddress: funTokenMapping.Erc20Addr.String(), - ToEthAddr: recipient.String(), - BankCoin: coin, - }) - - return &evm.MsgConvertCoinToEvmResponse{}, nil -} - // EmitEthereumTxEvents emits all types of EVM events applicable to a particular execution case func (k *Keeper) EmitEthereumTxEvents( ctx sdk.Context, @@ -721,3 +560,142 @@ func (k *Keeper) updateBlockBloom( k.EvmState.BlockLogSize.Set(ctx, blockLogSize) } } + +// CallContract invokes a smart contract on the method specified by [methodName] +// using the given [args]. +// +// Parameters: +// - ctx: The SDK context for the transaction. +// - abi: The ABI (Application Binary Interface) of the smart contract. +// - fromAcc: The Ethereum address of the account initiating the contract call. +// - contract: Pointer to the Ethereum address of the contract to be called. +// - commit: Boolean flag indicating whether to commit the transaction (true) or simulate it (false). +// - methodName: The name of the contract method to be called. +// - args: Variadic parameter for the arguments to be passed to the contract method. +// +// Note: This function handles both contract method calls and simulations, +// depending on the 'commit' parameter. +func (k Keeper) CallContract( + ctx sdk.Context, + abi *gethabi.ABI, + fromAcc gethcommon.Address, + contract *gethcommon.Address, + commit bool, + gasLimit uint64, + methodName string, + args ...any, +) (evmResp *evm.MsgEthereumTxResponse, err error) { + contractInput, err := abi.Pack(methodName, args...) + if err != nil { + return nil, fmt.Errorf("failed to pack ABI args: %w", err) + } + evmResp, _, err = k.CallContractWithInput(ctx, fromAcc, contract, commit, contractInput, gasLimit) + return evmResp, err +} + +// CallContractWithInput invokes a smart contract with the given [contractInput] +// or deploys a new contract. +// +// Parameters: +// - ctx: The SDK context for the transaction. +// - fromAcc: The Ethereum address of the account initiating the contract call. +// - contract: Pointer to the Ethereum address of the contract. Nil if new +// contract is deployed. +// - commit: Boolean flag indicating whether to commit the transaction (true) +// or simulate it (false). +// - contractInput: Hexadecimal-encoded bytes use as input data to the call. +// +// Note: This function handles both contract method calls and simulations, +// depending on the 'commit' parameter. It uses a default gas limit. +func (k Keeper) CallContractWithInput( + ctx sdk.Context, + fromAcc gethcommon.Address, + contract *gethcommon.Address, + commit bool, + contractInput []byte, + gasLimit uint64, +) (evmResp *evm.MsgEthereumTxResponse, evmObj *vm.EVM, err error) { + // This is a `defer` pattern to add behavior that runs in the case that the + // error is non-nil, creating a concise way to add extra information. + defer func() { + if err != nil { + err = fmt.Errorf("CallContractError: %w", err) + } + }() + nonce := k.GetAccNonce(ctx, fromAcc) + + unusedBigInt := big.NewInt(0) + evmMsg := gethcore.NewMessage( + fromAcc, + contract, + nonce, + unusedBigInt, // amount + gasLimit, + unusedBigInt, // gasFeeCap + unusedBigInt, // gasTipCap + unusedBigInt, // gasPrice + contractInput, + gethcore.AccessList{}, + !commit, // isFake + ) + + // Apply EVM message + evmCfg, err := k.GetEVMConfig( + ctx, + sdk.ConsAddress(ctx.BlockHeader().ProposerAddress), + k.EthChainID(ctx), + ) + if err != nil { + err = errors.Wrapf(err, "failed to load EVM config") + return + } + + // Generating TxConfig with an empty tx hash as there is no actual eth tx + // sent by a user + txConfig := k.TxConfig(ctx, gethcommon.BigToHash(big.NewInt(0))) + + // Using tmp context to not modify the state in case of evm revert + tmpCtx, commitCtx := ctx.CacheContext() + + evmResp, evmObj, err = k.ApplyEvmMsg( + tmpCtx, evmMsg, evm.NewNoOpTracer(), commit, evmCfg, txConfig, true, + ) + if err != nil { + // We don't know the actual gas used, so consuming the gas limit + k.ResetGasMeterAndConsumeGas(ctx, gasLimit) + err = errors.Wrap(err, "failed to apply ethereum core message") + return + } + if evmResp.Failed() { + k.ResetGasMeterAndConsumeGas(ctx, evmResp.GasUsed) + if evmResp.VmError != vm.ErrOutOfGas.Error() { + if evmResp.VmError == vm.ErrExecutionReverted.Error() { + err = fmt.Errorf("VMError: %w", evm.NewExecErrorWithReason(evmResp.Ret)) + return + } + err = fmt.Errorf("VMError: %s", evmResp.VmError) + return + } + err = fmt.Errorf("gas required exceeds allowance (%d)", gasLimit) + return + } else { + // Success, committing the state to ctx + if commit { + commitCtx() + totalGasUsed, err := k.AddToBlockGasUsed(ctx, evmResp.GasUsed) + if err != nil { + k.ResetGasMeterAndConsumeGas(ctx, ctx.GasMeter().Limit()) + return nil, nil, errors.Wrap(err, "error adding transient gas used to block") + } + k.ResetGasMeterAndConsumeGas(ctx, totalGasUsed) + k.updateBlockBloom(ctx, evmResp, uint64(txConfig.LogIndex)) + err = k.EmitEthereumTxEvents(ctx, contract, gethcore.LegacyTxType, evmMsg, evmResp) + if err != nil { + return nil, nil, errors.Wrap(err, "error emitting ethereum tx events") + } + blockTxIdx := uint64(txConfig.TxIndex) + 1 + k.EvmState.BlockTxIndex.Set(ctx, blockTxIdx) + } + return evmResp, evmObj, nil + } +} diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go index 5c585c2e9..97e20376f 100644 --- a/x/evm/precompile/funtoken.go +++ b/x/evm/precompile/funtoken.go @@ -28,6 +28,18 @@ var _ vm.PrecompiledContract = (*precompileFunToken)(nil) // using the ERC20's `FunToken` mapping. var PrecompileAddr_FunToken = gethcommon.HexToAddress("0x0000000000000000000000000000000000000800") +const ( + // FunTokenGasLimitBankSend consists of gas for 3 calls: + // 1. transfer erc20 from sender to module + // ~60_000 gas for regular erc20 transfer (our own ERC20Minter contract) + // could be higher for user created contracts, let's cap with 200_000 + // 2. mint native coin (made from erc20) or burn erc20 token (made from coin) + // ~60_000 gas for either mint or burn + // 3. send from module to account: + // ~65_000 gas (bank send) + FunTokenGasLimitBankSend uint64 = 400_000 +) + func (p precompileFunToken) Address() gethcommon.Address { return PrecompileAddr_FunToken } @@ -59,6 +71,10 @@ func (p precompileFunToken) Run( return nil, err } + // This handles any out of gas errors that may occur during the execution of a precompile tx or query. + // It avoids panics and returns the out of gas error so the EVM can continue gracefully. + defer HandleGasError(start.Ctx, contract, start.initialGas, &err)() + method := start.Method switch PrecompileMethod(method.Name) { case FunTokenMethod_BankSend: @@ -72,6 +88,12 @@ func (p precompileFunToken) Run( if err != nil { return nil, err } + + gasUsed := start.Ctx.GasMeter().GasConsumed() - start.initialGas + if !contract.UseGas(gasUsed) { + return nil, vm.ErrOutOfGas + } + // Dirty journal entries in `StateDB` must be committed return bz, start.StateDB.Commit() } diff --git a/x/evm/precompile/funtoken_test.go b/x/evm/precompile/funtoken_test.go index dd5176fb3..e3a4ff66f 100644 --- a/x/evm/precompile/funtoken_test.go +++ b/x/evm/precompile/funtoken_test.go @@ -8,6 +8,8 @@ import ( gethcommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/suite" + "github.com/NibiruChain/nibiru/v2/x/evm/keeper" + "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/x/common/testutil" "github.com/NibiruChain/nibiru/v2/x/common/testutil/testapp" @@ -113,7 +115,7 @@ func (s *FuntokenSuite) TestHappyPath() { input, err := embeds.SmartContract_ERC20Minter.ABI.Pack("mint", deps.Sender.EthAddr, big.NewInt(69_420)) s.NoError(err) _, _, err = deps.EvmKeeper.CallContractWithInput( - deps.Ctx, deps.Sender.EthAddr, &erc20, true, input, + deps.Ctx, deps.Sender.EthAddr, &erc20, true, input, keeper.Erc20GasLimitQuery, ) s.ErrorContains(err, "Ownable: caller is not the owner") } diff --git a/x/evm/precompile/oracle.go b/x/evm/precompile/oracle.go index fb0b2981b..c1824f2d6 100644 --- a/x/evm/precompile/oracle.go +++ b/x/evm/precompile/oracle.go @@ -31,6 +31,11 @@ const ( OracleMethod_queryExchangeRate PrecompileMethod = "queryExchangeRate" ) +const ( + // OracleGasLimitQuery is a rough limit. Actual gas used for this precompile is 22_880 + OracleGasLimitQuery uint64 = 100_000 +) + // Run runs the precompiled contract func (p precompileOracle) Run( evm *vm.EVM, contract *vm.Contract, readonly bool, @@ -38,11 +43,15 @@ func (p precompileOracle) Run( defer func() { err = ErrPrecompileRun(err, p) }() - res, err := OnRunStart(evm, contract, embeds.SmartContract_Oracle.ABI) + start, err := OnRunStart(evm, contract, embeds.SmartContract_Oracle.ABI) if err != nil { return nil, err } - method, args, ctx := res.Method, res.Args, res.Ctx + method, args, ctx := start.Method, start.Args, start.Ctx + + // This handles any out of gas errors that may occur during the execution of a precompile tx or query. + // It avoids panics and returns the out of gas error so the EVM can continue gracefully. + defer HandleGasError(start.Ctx, contract, start.initialGas, &err)() switch PrecompileMethod(method.Name) { case OracleMethod_queryExchangeRate: @@ -52,6 +61,11 @@ func (p precompileOracle) Run( return } + gasUsed := start.Ctx.GasMeter().GasConsumed() - start.initialGas + if !contract.UseGas(gasUsed) { + return nil, vm.ErrOutOfGas + } + return } diff --git a/x/evm/precompile/oracle_test.go b/x/evm/precompile/oracle_test.go index efab80118..c68d3db74 100644 --- a/x/evm/precompile/oracle_test.go +++ b/x/evm/precompile/oracle_test.go @@ -59,7 +59,12 @@ func (s *OracleSuite) TestOracle_HappyPath() { input, err := embeds.SmartContract_Oracle.ABI.Pack("queryExchangeRate", "unibi:uusd") s.NoError(err) resp, _, err := deps.EvmKeeper.CallContractWithInput( - deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Oracle, true, input, + deps.Ctx, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_Oracle, + false, + input, + precompile.OracleGasLimitQuery, ) s.NoError(err) diff --git a/x/evm/precompile/precompile.go b/x/evm/precompile/precompile.go index a6bbfefc4..fd04cc5a7 100644 --- a/x/evm/precompile/precompile.go +++ b/x/evm/precompile/precompile.go @@ -19,6 +19,7 @@ import ( "github.com/NibiruChain/collections" store "github.com/cosmos/cosmos-sdk/store/types" + storetypes "github.com/cosmos/cosmos-sdk/store/types" gethabi "github.com/ethereum/go-ethereum/accounts/abi" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" @@ -147,6 +148,8 @@ type OnRunStartResult struct { Method *gethabi.Method StateDB *statedb.StateDB + + initialGas storetypes.Gas } // OnRunStart prepares the execution environment for a precompiled contract call. @@ -193,11 +196,23 @@ func OnRunStart( return res, fmt.Errorf("error committing dirty journal entries: %w", err) } + initialGas := ctx.GasMeter().GasConsumed() + + defer HandleGasError(ctx, contract, initialGas, &err)() + + // set the default SDK gas configuration to track gas usage + // we are changing the gas meter type, so it panics gracefully when out of gas + ctx = ctx.WithGasMeter(storetypes.NewGasMeter(contract.Gas)). + WithKVGasConfig(storetypes.GasConfig{}). + WithTransientKVGasConfig(storetypes.GasConfig{}) + ctx.GasMeter().ConsumeGas(initialGas, "creating a new gas meter") + return OnRunStartResult{ - Args: args, - Ctx: ctx, - Method: method, - StateDB: stateDB, + Args: args, + Ctx: ctx, + Method: method, + StateDB: stateDB, + initialGas: initialGas, }, nil } @@ -212,3 +227,25 @@ var precompileMethodIsTxMap map[PrecompileMethod]bool = map[PrecompileMethod]boo OracleMethod_queryExchangeRate: false, } + +// HandleGasError handles the out of gas panic by resetting the gas meter and returning an error. +// This is used in order to avoid panics and to allow for the EVM to continue cleanup if the tx or query run out of gas. +func HandleGasError(ctx sdk.Context, contract *vm.Contract, initialGas storetypes.Gas, err *error) func() { + return func() { + if r := recover(); r != nil { + switch r.(type) { + case storetypes.ErrorOutOfGas: + // update contract gas + usedGas := ctx.GasMeter().GasConsumed() - initialGas + _ = contract.UseGas(usedGas) + + *err = vm.ErrOutOfGas + // FIXME: add InfiniteGasMeter with previous Gas limit. + ctx = ctx.WithKVGasConfig(storetypes.GasConfig{}). + WithTransientKVGasConfig(storetypes.GasConfig{}) + default: + panic(r) + } + } + } +} diff --git a/x/evm/precompile/test/export.go b/x/evm/precompile/test/export.go index 966dd3359..435e60bfb 100644 --- a/x/evm/precompile/test/export.go +++ b/x/evm/precompile/test/export.go @@ -71,7 +71,12 @@ func SetupWasmContracts(deps *evmtest.TestDeps, s *suite.Suite) ( s.Require().NoError(err) ethTxResp, evmObj, err := deps.EvmKeeper.CallContractWithInput( - deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + deps.Ctx, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_Wasm, + true, + input, + precompile.WasmGasLimitInstantiate, ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) @@ -186,7 +191,12 @@ func AssertWasmCounterState( s.Require().NoError(err) ethTxResp, evmObj, err := deps.EvmKeeper.CallContractWithInput( - deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + deps.Ctx, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_Wasm, + true, + input, + precompile.WasmGasLimitInstantiate, ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) @@ -308,7 +318,12 @@ func IncrementWasmCounterWithExecuteMulti( s.Require().NoError(err) ethTxResp, evmObj, err := deps.EvmKeeper.CallContractWithInput( - deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + deps.Ctx, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_Wasm, + true, + input, + precompile.WasmGasLimitExecute, ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) diff --git a/x/evm/precompile/wasm.go b/x/evm/precompile/wasm.go index 10817c673..42ea57081 100644 --- a/x/evm/precompile/wasm.go +++ b/x/evm/precompile/wasm.go @@ -29,6 +29,14 @@ const ( WasmMethod_queryRaw PrecompileMethod = "queryRaw" ) +const ( + // rough gas limits for wasm execution + + WasmGasLimitInstantiate uint64 = 10_000_000 + WasmGasLimitQuery uint64 = 10_000_000 + WasmGasLimitExecute uint64 = 10_000_000 +) + // Run runs the precompiled contract func (p precompileWasm) Run( evm *vm.EVM, contract *vm.Contract, readonly bool, @@ -42,6 +50,10 @@ func (p precompileWasm) Run( } method := start.Method + // This handles any out of gas errors that may occur during the execution of a precompile tx or query. + // It avoids panics and returns the out of gas error so the EVM can continue gracefully. + defer HandleGasError(start.Ctx, contract, start.initialGas, &err)() + switch PrecompileMethod(method.Name) { case WasmMethod_execute: bz, err = p.execute(start, contract.CallerAddress, readonly) @@ -63,6 +75,11 @@ func (p precompileWasm) Run( return nil, err } + gasUsed := start.Ctx.GasMeter().GasConsumed() - start.initialGas + if !contract.UseGas(gasUsed) { + return nil, vm.ErrOutOfGas + } + // Dirty journal entries in `StateDB` must be committed return bz, start.StateDB.Commit() } diff --git a/x/evm/precompile/wasm_test.go b/x/evm/precompile/wasm_test.go index d796f8b89..effc2f6c5 100644 --- a/x/evm/precompile/wasm_test.go +++ b/x/evm/precompile/wasm_test.go @@ -53,7 +53,12 @@ func (s *WasmSuite) TestExecuteHappy() { s.Require().NoError(err) ethTxResp, _, err := deps.EvmKeeper.CallContractWithInput( - deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + deps.Ctx, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_Wasm, + true, + input, + precompile.WasmGasLimitExecute, ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) @@ -82,7 +87,12 @@ func (s *WasmSuite) TestExecuteHappy() { ) s.Require().NoError(err) ethTxResp, _, err = deps.EvmKeeper.CallContractWithInput( - deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + deps.Ctx, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_Wasm, + true, + input, + precompile.WasmGasLimitExecute, ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) @@ -137,7 +147,12 @@ func (s *WasmSuite) assertWasmCounterStateRaw( s.Require().NoError(err) ethTxResp, _, err := deps.EvmKeeper.CallContractWithInput( - deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + deps.Ctx, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_Wasm, + true, + input, + precompile.WasmGasLimitQuery, ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) @@ -307,7 +322,12 @@ func (s *WasmSuite) TestSadArgsExecute() { s.Require().NoError(err) ethTxResp, _, err := deps.EvmKeeper.CallContractWithInput( - deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + deps.Ctx, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_Wasm, + true, + input, + precompile.WasmGasLimitExecute, ) s.Require().ErrorContains(err, tc.wantError, "ethTxResp %v", ethTxResp) }) diff --git a/x/evm/statedb/journal_test.go b/x/evm/statedb/journal_test.go index 5863face5..a8185097a 100644 --- a/x/evm/statedb/journal_test.go +++ b/x/evm/statedb/journal_test.go @@ -9,6 +9,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/NibiruChain/nibiru/v2/x/evm/keeper" + serverconfig "github.com/NibiruChain/nibiru/v2/app/server/config" "github.com/NibiruChain/nibiru/v2/x/common/testutil/testapp" "github.com/NibiruChain/nibiru/v2/x/evm" @@ -65,7 +67,12 @@ func (s *Suite) TestPrecompileSnapshots() { input, err := deps.EvmKeeper.ERC20().ABI.Pack("mint", to, amount) s.Require().NoError(err) _, evmObj, err := deps.EvmKeeper.CallContractWithInput( - deps.Ctx, deps.Sender.EthAddr, &contract, true, input, + deps.Ctx, + deps.Sender.EthAddr, + &contract, + true, + input, + keeper.Erc20GasLimitExecute, ) s.Require().NoError(err) From c27bdad7c0e7f1f581351980a486fddc202499b0 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Sat, 26 Oct 2024 18:36:09 +0400 Subject: [PATCH 2/5] fix: local precompile gas meters --- CHANGELOG.md | 1 + x/evm/precompile/funtoken.go | 9 +++++--- x/evm/precompile/oracle.go | 5 +++-- x/evm/precompile/precompile.go | 38 +++++++++++++++------------------- x/evm/precompile/wasm.go | 5 +++-- 5 files changed, 30 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 821b29e13..db348a90f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ consistent setup and dynamic gas calculations, addressing the following tickets. - [#2088](https://github.com/NibiruChain/nibiru/pull/2088) - refactor(evm): remove outdated comment and improper error message text - [#2089](https://github.com/NibiruChain/nibiru/pull/2089) - better handling of gas consumption within erc20 contract execution - [#2091](https://github.com/NibiruChain/nibiru/pull/2091) - feat(evm): add fun token creation fee validation +- [#2093](https://github.com/NibiruChain/nibiru/pull/2093) - feat(evm): gas usage in precompiles: limits, local gas meters #### Nibiru EVM | Before Audit 1 - 2024-10-18 diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go index 97e20376f..9fd0516cc 100644 --- a/x/evm/precompile/funtoken.go +++ b/x/evm/precompile/funtoken.go @@ -37,7 +37,9 @@ const ( // ~60_000 gas for either mint or burn // 3. send from module to account: // ~65_000 gas (bank send) - FunTokenGasLimitBankSend uint64 = 400_000 + // additional cosmos calls seem to bump the total value a way higher + // TODO (ON): check deeper + FunTokenGasLimitBankSend uint64 = 2_000_000 ) func (p precompileFunToken) Address() gethcommon.Address { @@ -73,7 +75,7 @@ func (p precompileFunToken) Run( // This handles any out of gas errors that may occur during the execution of a precompile tx or query. // It avoids panics and returns the out of gas error so the EVM can continue gracefully. - defer HandleGasError(start.Ctx, contract, start.initialGas, &err)() + defer ReturnToParentGasMeter(start.Ctx, contract, start.parentGasMeter, &err)() method := start.Method switch PrecompileMethod(method.Name) { @@ -89,7 +91,8 @@ func (p precompileFunToken) Run( return nil, err } - gasUsed := start.Ctx.GasMeter().GasConsumed() - start.initialGas + // Gas consumed by a local gas meter + gasUsed := start.Ctx.GasMeter().GasConsumed() if !contract.UseGas(gasUsed) { return nil, vm.ErrOutOfGas } diff --git a/x/evm/precompile/oracle.go b/x/evm/precompile/oracle.go index c1824f2d6..987e7f75a 100644 --- a/x/evm/precompile/oracle.go +++ b/x/evm/precompile/oracle.go @@ -51,7 +51,7 @@ func (p precompileOracle) Run( // This handles any out of gas errors that may occur during the execution of a precompile tx or query. // It avoids panics and returns the out of gas error so the EVM can continue gracefully. - defer HandleGasError(start.Ctx, contract, start.initialGas, &err)() + defer ReturnToParentGasMeter(start.Ctx, contract, start.parentGasMeter, &err)() switch PrecompileMethod(method.Name) { case OracleMethod_queryExchangeRate: @@ -61,7 +61,8 @@ func (p precompileOracle) Run( return } - gasUsed := start.Ctx.GasMeter().GasConsumed() - start.initialGas + // Gas consumed by a local gas meter + gasUsed := start.Ctx.GasMeter().GasConsumed() if !contract.UseGas(gasUsed) { return nil, vm.ErrOutOfGas } diff --git a/x/evm/precompile/precompile.go b/x/evm/precompile/precompile.go index fd04cc5a7..02ae636af 100644 --- a/x/evm/precompile/precompile.go +++ b/x/evm/precompile/precompile.go @@ -149,7 +149,7 @@ type OnRunStartResult struct { StateDB *statedb.StateDB - initialGas storetypes.Gas + parentGasMeter sdk.GasMeter } // OnRunStart prepares the execution environment for a precompiled contract call. @@ -196,23 +196,19 @@ func OnRunStart( return res, fmt.Errorf("error committing dirty journal entries: %w", err) } - initialGas := ctx.GasMeter().GasConsumed() - - defer HandleGasError(ctx, contract, initialGas, &err)() - - // set the default SDK gas configuration to track gas usage - // we are changing the gas meter type, so it panics gracefully when out of gas + // Temporarily switching to a local gas meter to enforce gas limit check for a precompile + // returning parent gas meter after execution or failure + parentGasMeter := ctx.GasMeter() ctx = ctx.WithGasMeter(storetypes.NewGasMeter(contract.Gas)). WithKVGasConfig(storetypes.GasConfig{}). WithTransientKVGasConfig(storetypes.GasConfig{}) - ctx.GasMeter().ConsumeGas(initialGas, "creating a new gas meter") return OnRunStartResult{ - Args: args, - Ctx: ctx, - Method: method, - StateDB: stateDB, - initialGas: initialGas, + Args: args, + Ctx: ctx, + Method: method, + StateDB: stateDB, + parentGasMeter: parentGasMeter, }, nil } @@ -228,24 +224,24 @@ var precompileMethodIsTxMap map[PrecompileMethod]bool = map[PrecompileMethod]boo OracleMethod_queryExchangeRate: false, } -// HandleGasError handles the out of gas panic by resetting the gas meter and returning an error. +// ReturnToParentGasMeter resets the ctx.GasMeter back to a parent GasMeter before precompile execution. +// Additionally, handles the out of gas panic by resetting the gas meter and returning an error. // This is used in order to avoid panics and to allow for the EVM to continue cleanup if the tx or query run out of gas. -func HandleGasError(ctx sdk.Context, contract *vm.Contract, initialGas storetypes.Gas, err *error) func() { +func ReturnToParentGasMeter(ctx sdk.Context, contract *vm.Contract, parentGasMeter sdk.GasMeter, err *error) func() { return func() { if r := recover(); r != nil { switch r.(type) { case storetypes.ErrorOutOfGas: - // update contract gas - usedGas := ctx.GasMeter().GasConsumed() - initialGas - _ = contract.UseGas(usedGas) + _ = contract.UseGas(ctx.GasMeter().GasConsumed()) *err = vm.ErrOutOfGas - // FIXME: add InfiniteGasMeter with previous Gas limit. - ctx = ctx.WithKVGasConfig(storetypes.GasConfig{}). - WithTransientKVGasConfig(storetypes.GasConfig{}) default: panic(r) } } + // Back to parent ctx gas meter (before entering precompile) + ctx = ctx.WithGasMeter(parentGasMeter). + WithKVGasConfig(storetypes.GasConfig{}). + WithTransientKVGasConfig(storetypes.GasConfig{}) } } diff --git a/x/evm/precompile/wasm.go b/x/evm/precompile/wasm.go index 42ea57081..3982c4efc 100644 --- a/x/evm/precompile/wasm.go +++ b/x/evm/precompile/wasm.go @@ -52,7 +52,7 @@ func (p precompileWasm) Run( // This handles any out of gas errors that may occur during the execution of a precompile tx or query. // It avoids panics and returns the out of gas error so the EVM can continue gracefully. - defer HandleGasError(start.Ctx, contract, start.initialGas, &err)() + defer ReturnToParentGasMeter(start.Ctx, contract, start.parentGasMeter, &err)() switch PrecompileMethod(method.Name) { case WasmMethod_execute: @@ -75,7 +75,8 @@ func (p precompileWasm) Run( return nil, err } - gasUsed := start.Ctx.GasMeter().GasConsumed() - start.initialGas + // Gas consumed by a local gas meter + gasUsed := start.Ctx.GasMeter().GasConsumed() if !contract.UseGas(gasUsed) { return nil, vm.ErrOutOfGas } From abd2d91f94afb504bc4b49b0b895498038c61ed4 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Mon, 28 Oct 2024 20:32:57 +0400 Subject: [PATCH 3/5] test(precompile): tests for precompile local gas meters --- .../TestFunTokenPrecompileLocalGas.json | 63 +++++++++ .../TestFunTokenPrecompileLocalGas.sol | 47 +++++++ x/evm/embeds/embeds.go | 9 ++ x/evm/embeds/embeds_test.go | 1 + x/evm/evmtest/test_deps.go | 5 + x/evm/keeper/funtoken_from_coin_test.go | 2 + x/evm/keeper/funtoken_from_erc20_test.go | 8 ++ x/evm/keeper/msg_server.go | 3 +- x/evm/precompile/funtoken.go | 7 +- x/evm/precompile/funtoken_test.go | 120 ++++++++++++++---- x/evm/precompile/oracle.go | 8 +- x/evm/precompile/oracle_test.go | 4 +- x/evm/precompile/test/export.go | 14 +- x/evm/precompile/wasm.go | 11 +- x/evm/precompile/wasm_test.go | 16 ++- 15 files changed, 264 insertions(+), 54 deletions(-) create mode 100644 x/evm/embeds/artifacts/contracts/TestFunTokenPrecompileLocalGas.sol/TestFunTokenPrecompileLocalGas.json create mode 100644 x/evm/embeds/contracts/TestFunTokenPrecompileLocalGas.sol diff --git a/x/evm/embeds/artifacts/contracts/TestFunTokenPrecompileLocalGas.sol/TestFunTokenPrecompileLocalGas.json b/x/evm/embeds/artifacts/contracts/TestFunTokenPrecompileLocalGas.sol/TestFunTokenPrecompileLocalGas.json new file mode 100644 index 000000000..970a31649 --- /dev/null +++ b/x/evm/embeds/artifacts/contracts/TestFunTokenPrecompileLocalGas.sol/TestFunTokenPrecompileLocalGas.json @@ -0,0 +1,63 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "TestFunTokenPrecompileLocalGas", + "sourceName": "contracts/TestFunTokenPrecompileLocalGas.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "erc20_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "string", + "name": "bech32Recipient", + "type": "string" + } + ], + "name": "callBankSend", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "string", + "name": "bech32Recipient", + "type": "string" + }, + { + "internalType": "uint256", + "name": "customGas", + "type": "uint256" + } + ], + "name": "callBankSendLocalGas", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x608060405234801561001057600080fd5b50604051610951380380610951833981810160405281019061003291906100db565b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050610108565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100a88261007d565b9050919050565b6100b88161009d565b81146100c357600080fd5b50565b6000815190506100d5816100af565b92915050565b6000602082840312156100f1576100f0610078565b5b60006100ff848285016100c6565b91505092915050565b61083a806101176000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806359b6ed891461003b57806390d2b5e714610057575b600080fd5b610055600480360381019061005091906104d0565b610073565b005b610071600480360381019061006c919061053f565b6101db565b005b600061080073ffffffffffffffffffffffffffffffffffffffff168260008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1686866040516024016100c49392919061066a565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161014e91906106ef565b60006040518083038160008787f1925050503d806000811461018c576040519150601f19603f3d011682016040523d82523d6000602084013e610191565b606091505b50509050806101d5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101cc90610778565b60405180910390fd5b50505050565b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff16848460405160240161022b9392919061066a565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040516102b591906106ef565b6000604051808303816000865af19150503d80600081146102f2576040519150601f19603f3d011682016040523d82523d6000602084013e6102f7565b606091505b505090508061033b576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610332906107e4565b60405180910390fd5b505050565b6000604051905090565b600080fd5b600080fd5b6000819050919050565b61036781610354565b811461037257600080fd5b50565b6000813590506103848161035e565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6103dd82610394565b810181811067ffffffffffffffff821117156103fc576103fb6103a5565b5b80604052505050565b600061040f610340565b905061041b82826103d4565b919050565b600067ffffffffffffffff82111561043b5761043a6103a5565b5b61044482610394565b9050602081019050919050565b82818337600083830152505050565b600061047361046e84610420565b610405565b90508281526020810184848401111561048f5761048e61038f565b5b61049a848285610451565b509392505050565b600082601f8301126104b7576104b661038a565b5b81356104c7848260208601610460565b91505092915050565b6000806000606084860312156104e9576104e861034a565b5b60006104f786828701610375565b935050602084013567ffffffffffffffff8111156105185761051761034f565b5b610524868287016104a2565b925050604061053586828701610375565b9150509250925092565b600080604083850312156105565761055561034a565b5b600061056485828601610375565b925050602083013567ffffffffffffffff8111156105855761058461034f565b5b610591858286016104a2565b9150509250929050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006105c68261059b565b9050919050565b6105d6816105bb565b82525050565b6105e581610354565b82525050565b600081519050919050565b600082825260208201905092915050565b60005b8381101561062557808201518184015260208101905061060a565b60008484015250505050565b600061063c826105eb565b61064681856105f6565b9350610656818560208601610607565b61065f81610394565b840191505092915050565b600060608201905061067f60008301866105cd565b61068c60208301856105dc565b818103604083015261069e8184610631565b9050949350505050565b600081519050919050565b600081905092915050565b60006106c9826106a8565b6106d381856106b3565b93506106e3818560208601610607565b80840191505092915050565b60006106fb82846106be565b915081905092915050565b7f4661696c656420746f2063616c6c2062616e6b53656e6420776974682063757360008201527f746f6d2067617300000000000000000000000000000000000000000000000000602082015250565b60006107626027836105f6565b915061076d82610706565b604082019050919050565b6000602082019050818103600083015261079181610755565b9050919050565b7f4661696c656420746f2063616c6c2062616e6b53656e64000000000000000000600082015250565b60006107ce6017836105f6565b91506107d982610798565b602082019050919050565b600060208201905081810360008301526107fd816107c1565b905091905056fea2646970667358221220cf16927fc50953575dc9f444e1aefe75fa60bf10c7688dd039cb7503669ab76964736f6c63430008180033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100365760003560e01c806359b6ed891461003b57806390d2b5e714610057575b600080fd5b610055600480360381019061005091906104d0565b610073565b005b610071600480360381019061006c919061053f565b6101db565b005b600061080073ffffffffffffffffffffffffffffffffffffffff168260008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1686866040516024016100c49392919061066a565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161014e91906106ef565b60006040518083038160008787f1925050503d806000811461018c576040519150601f19603f3d011682016040523d82523d6000602084013e610191565b606091505b50509050806101d5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101cc90610778565b60405180910390fd5b50505050565b600061080073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff16848460405160240161022b9392919061066a565b6040516020818303038152906040527f03003bc5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040516102b591906106ef565b6000604051808303816000865af19150503d80600081146102f2576040519150601f19603f3d011682016040523d82523d6000602084013e6102f7565b606091505b505090508061033b576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610332906107e4565b60405180910390fd5b505050565b6000604051905090565b600080fd5b600080fd5b6000819050919050565b61036781610354565b811461037257600080fd5b50565b6000813590506103848161035e565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6103dd82610394565b810181811067ffffffffffffffff821117156103fc576103fb6103a5565b5b80604052505050565b600061040f610340565b905061041b82826103d4565b919050565b600067ffffffffffffffff82111561043b5761043a6103a5565b5b61044482610394565b9050602081019050919050565b82818337600083830152505050565b600061047361046e84610420565b610405565b90508281526020810184848401111561048f5761048e61038f565b5b61049a848285610451565b509392505050565b600082601f8301126104b7576104b661038a565b5b81356104c7848260208601610460565b91505092915050565b6000806000606084860312156104e9576104e861034a565b5b60006104f786828701610375565b935050602084013567ffffffffffffffff8111156105185761051761034f565b5b610524868287016104a2565b925050604061053586828701610375565b9150509250925092565b600080604083850312156105565761055561034a565b5b600061056485828601610375565b925050602083013567ffffffffffffffff8111156105855761058461034f565b5b610591858286016104a2565b9150509250929050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006105c68261059b565b9050919050565b6105d6816105bb565b82525050565b6105e581610354565b82525050565b600081519050919050565b600082825260208201905092915050565b60005b8381101561062557808201518184015260208101905061060a565b60008484015250505050565b600061063c826105eb565b61064681856105f6565b9350610656818560208601610607565b61065f81610394565b840191505092915050565b600060608201905061067f60008301866105cd565b61068c60208301856105dc565b818103604083015261069e8184610631565b9050949350505050565b600081519050919050565b600081905092915050565b60006106c9826106a8565b6106d381856106b3565b93506106e3818560208601610607565b80840191505092915050565b60006106fb82846106be565b915081905092915050565b7f4661696c656420746f2063616c6c2062616e6b53656e6420776974682063757360008201527f746f6d2067617300000000000000000000000000000000000000000000000000602082015250565b60006107626027836105f6565b915061076d82610706565b604082019050919050565b6000602082019050818103600083015261079181610755565b9050919050565b7f4661696c656420746f2063616c6c2062616e6b53656e64000000000000000000600082015250565b60006107ce6017836105f6565b91506107d982610798565b602082019050919050565b600060208201905081810360008301526107fd816107c1565b905091905056fea2646970667358221220cf16927fc50953575dc9f444e1aefe75fa60bf10c7688dd039cb7503669ab76964736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/x/evm/embeds/contracts/TestFunTokenPrecompileLocalGas.sol b/x/evm/embeds/contracts/TestFunTokenPrecompileLocalGas.sol new file mode 100644 index 000000000..00c1d735d --- /dev/null +++ b/x/evm/embeds/contracts/TestFunTokenPrecompileLocalGas.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./FunToken.sol"; + +contract TestFunTokenPrecompileLocalGas { + address erc20; + + constructor(address erc20_) { + erc20 = erc20_; + } + + // Calls bankSend of the FunToken Precompile with the default gas. + // Internal call could use all the gas for the parent call. + function callBankSend( + uint256 amount, + string memory bech32Recipient + ) public { + (bool success,) = FUNTOKEN_PRECOMPILE_ADDRESS.call( + abi.encodeWithSignature( + "bankSend(address,uint256,string)", + erc20, + amount, + bech32Recipient + ) + ); + require(success, "Failed to call bankSend"); + } + + // Calls bankSend of the FunToken Precompile with the gas amount set in parameter. + // Internal call should fail if the gas provided is insufficient. + function callBankSendLocalGas( + uint256 amount, + string memory bech32Recipient, + uint256 customGas + ) public { + (bool success,) = FUNTOKEN_PRECOMPILE_ADDRESS.call{gas: customGas}( + abi.encodeWithSignature( + "bankSend(address,uint256,string)", + erc20, + amount, + bech32Recipient + ) + ); + require(success, "Failed to call bankSend with custom gas"); + } +} \ No newline at end of file diff --git a/x/evm/embeds/embeds.go b/x/evm/embeds/embeds.go index 5ac65655d..bd4a2873a 100644 --- a/x/evm/embeds/embeds.go +++ b/x/evm/embeds/embeds.go @@ -29,6 +29,8 @@ var ( testErc20MaliciousNameJson []byte //go:embed artifacts/contracts/TestERC20MaliciousTransfer.sol/TestERC20MaliciousTransfer.json testErc20MaliciousTransferJson []byte + //go:embed artifacts/contracts/TestFunTokenPrecompileLocalGas.sol/TestFunTokenPrecompileLocalGas.json + testFunTokenPrecompileLocalGasJson []byte ) var ( @@ -76,6 +78,12 @@ var ( Name: "TestERC20MaliciousTransfer.sol", EmbedJSON: testErc20MaliciousTransferJson, } + // SmartContract_TestFunTokenPrecompileLocalGas is a test contract + // which allows precompile execution with custom local gas set (calling precompile within contract) + SmartContract_TestFunTokenPrecompileLocalGas = CompiledEvmContract{ + Name: "TestFunTokenPrecompileLocalGas.sol", + EmbedJSON: testFunTokenPrecompileLocalGasJson, + } ) func init() { @@ -86,6 +94,7 @@ func init() { SmartContract_TestERC20.MustLoad() SmartContract_TestERC20MaliciousName.MustLoad() SmartContract_TestERC20MaliciousTransfer.MustLoad() + SmartContract_TestFunTokenPrecompileLocalGas.MustLoad() } type CompiledEvmContract struct { diff --git a/x/evm/embeds/embeds_test.go b/x/evm/embeds/embeds_test.go index 586ff68b9..e3006dd9e 100644 --- a/x/evm/embeds/embeds_test.go +++ b/x/evm/embeds/embeds_test.go @@ -16,5 +16,6 @@ func TestLoadContracts(t *testing.T) { embeds.SmartContract_TestERC20.MustLoad() embeds.SmartContract_TestERC20MaliciousName.MustLoad() embeds.SmartContract_TestERC20MaliciousTransfer.MustLoad() + embeds.SmartContract_TestFunTokenPrecompileLocalGas.MustLoad() }) } diff --git a/x/evm/evmtest/test_deps.go b/x/evm/evmtest/test_deps.go index 1810b1c8f..5e238cca9 100644 --- a/x/evm/evmtest/test_deps.go +++ b/x/evm/evmtest/test_deps.go @@ -60,3 +60,8 @@ func (deps *TestDeps) GethSigner() gethcore.Signer { func (deps TestDeps) GoCtx() context.Context { return sdk.WrapSDKContext(deps.Ctx) } + +func (deps TestDeps) ResetGasMeter() { + deps.EvmKeeper.ResetTransientGasUsed(deps.Ctx) + deps.EvmKeeper.ResetGasMeterAndConsumeGas(deps.Ctx, 0) +} diff --git a/x/evm/keeper/funtoken_from_coin_test.go b/x/evm/keeper/funtoken_from_coin_test.go index 1f65efe7e..070f4c960 100644 --- a/x/evm/keeper/funtoken_from_coin_test.go +++ b/x/evm/keeper/funtoken_from_coin_test.go @@ -257,6 +257,8 @@ func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() { ) s.Require().ErrorContains(err, "insufficient funds") + deps.ResetGasMeter() + s.T().Log("Convert erc-20 to back to bank coin") _, err = deps.EvmKeeper.CallContract( deps.Ctx, diff --git a/x/evm/keeper/funtoken_from_erc20_test.go b/x/evm/keeper/funtoken_from_erc20_test.go index 1d1659fad..9f798835e 100644 --- a/x/evm/keeper/funtoken_from_erc20_test.go +++ b/x/evm/keeper/funtoken_from_erc20_test.go @@ -211,6 +211,8 @@ func (s *FunTokenFromErc20Suite) TestSendFromEvmToBank() { randomAcc := testutil.AccAddress() + deps.ResetGasMeter() + s.T().Log("send erc20 tokens to Bank") _, err = deps.EvmKeeper.CallContract( deps.Ctx, @@ -233,6 +235,8 @@ func (s *FunTokenFromErc20Suite) TestSendFromEvmToBank() { deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, bankDemon).Amount, ) + deps.ResetGasMeter() + s.T().Log("sad: send too many erc20 tokens to Bank") evmResp, err := deps.EvmKeeper.CallContract( deps.Ctx, @@ -249,6 +253,8 @@ func (s *FunTokenFromErc20Suite) TestSendFromEvmToBank() { s.T().Log("check balances") s.Require().Error(err, evmResp.String()) + deps.ResetGasMeter() + s.T().Log("send Bank tokens back to erc20") _, err = deps.EvmKeeper.ConvertCoinToEvm(sdk.WrapSDKContext(deps.Ctx), &evm.MsgConvertCoinToEvm{ @@ -361,6 +367,8 @@ func (s *FunTokenFromErc20Suite) TestFunTokenFromERC20MaliciousTransfer() { s.Require().NoError(err) randomAcc := testutil.AccAddress() + deps.ResetGasMeter() + s.T().Log("send erc20 tokens to cosmos") _, err = deps.EvmKeeper.CallContract( deps.Ctx, diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 2fbf2909b..b55889ec2 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -7,6 +7,7 @@ import ( "fmt" "math/big" "strconv" + "strings" "cosmossdk.io/errors" "cosmossdk.io/math" @@ -668,7 +669,7 @@ func (k Keeper) CallContractWithInput( } if evmResp.Failed() { k.ResetGasMeterAndConsumeGas(ctx, evmResp.GasUsed) - if evmResp.VmError != vm.ErrOutOfGas.Error() { + if !strings.Contains(evmResp.VmError, vm.ErrOutOfGas.Error()) { if evmResp.VmError == vm.ErrExecutionReverted.Error() { err = fmt.Errorf("VMError: %w", evm.NewExecErrorWithReason(evmResp.Ret)) return diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go index 9fd0516cc..d1085700b 100644 --- a/x/evm/precompile/funtoken.go +++ b/x/evm/precompile/funtoken.go @@ -37,9 +37,7 @@ const ( // ~60_000 gas for either mint or burn // 3. send from module to account: // ~65_000 gas (bank send) - // additional cosmos calls seem to bump the total value a way higher - // TODO (ON): check deeper - FunTokenGasLimitBankSend uint64 = 2_000_000 + FunTokenGasLimitBankSend uint64 = 200_000 ) func (p precompileFunToken) Address() gethcommon.Address { @@ -73,8 +71,7 @@ func (p precompileFunToken) Run( return nil, err } - // This handles any out of gas errors that may occur during the execution of a precompile tx or query. - // It avoids panics and returns the out of gas error so the EVM can continue gracefully. + // Resets the gas meter to parent one after precompile execution and gracefully handles "out of gas" defer ReturnToParentGasMeter(start.Ctx, contract, start.parentGasMeter, &err)() method := start.Method diff --git a/x/evm/precompile/funtoken_test.go b/x/evm/precompile/funtoken_test.go index e3a4ff66f..4f5492a8b 100644 --- a/x/evm/precompile/funtoken_test.go +++ b/x/evm/precompile/funtoken_test.go @@ -27,6 +27,16 @@ func TestSuite(t *testing.T) { type FuntokenSuite struct { suite.Suite + deps evmtest.TestDeps + funtoken evm.FunToken +} + +func (s *FuntokenSuite) SetupSuite() { + s.deps = evmtest.NewTestDeps() + + s.T().Log("Create FunToken from coin") + bankDenom := "unibi" + s.funtoken = evmtest.CreateFunTokenForBankCoin(&s.deps, bankDenom, &s.Suite) } func (s *FuntokenSuite) TestFailToPackABI() { @@ -80,12 +90,8 @@ func (s *FuntokenSuite) TestFailToPackABI() { } func (s *FuntokenSuite) TestHappyPath() { - deps := evmtest.NewTestDeps() - - s.T().Log("Create FunToken mapping and ERC20") - bankDenom := "unibi" - funtoken := evmtest.CreateFunTokenForBankCoin(&deps, bankDenom, &s.Suite) - erc20 := funtoken.Erc20Addr.Address + deps := s.deps + erc20 := s.funtoken.Erc20Addr.Address s.T().Log("Balances of the ERC20 should start empty") evmtest.AssertERC20BalanceEqual(s.T(), deps, erc20, deps.Sender.EthAddr, big.NewInt(0)) @@ -95,14 +101,14 @@ func (s *FuntokenSuite) TestHappyPath() { deps.App.BankKeeper, deps.Ctx, deps.Sender.NibiruAddr, - sdk.NewCoins(sdk.NewCoin(bankDenom, sdk.NewInt(69_420))), + sdk.NewCoins(sdk.NewCoin(s.funtoken.BankDenom, sdk.NewInt(69_420))), )) _, err := deps.EvmKeeper.ConvertCoinToEvm( sdk.WrapSDKContext(deps.Ctx), &evm.MsgConvertCoinToEvm{ Sender: deps.Sender.NibiruAddr.String(), - BankCoin: sdk.NewCoin(bankDenom, sdk.NewInt(69_420)), + BankCoin: sdk.NewCoin(s.funtoken.BankDenom, sdk.NewInt(69_420)), ToEthAddr: eth.EIP55Addr{ Address: deps.Sender.EthAddr, }, @@ -123,23 +129,91 @@ func (s *FuntokenSuite) TestHappyPath() { randomAcc := testutil.AccAddress() s.T().Log("Send using precompile") - amtToSend := int64(420) - callArgs := []any{erc20, big.NewInt(amtToSend), randomAcc.String()} - input, err := embeds.SmartContract_FunToken.ABI.Pack(string(precompile.FunTokenMethod_BankSend), callArgs...) - s.NoError(err) - - _, resp, err := evmtest.CallContractTx( - &deps, - precompile.PrecompileAddr_FunToken, - input, - deps.Sender, + { + amtToSend := int64(420) + callArgs := []any{erc20, big.NewInt(amtToSend), randomAcc.String()} + input, err := embeds.SmartContract_FunToken.ABI.Pack(string(precompile.FunTokenMethod_BankSend), callArgs...) + s.NoError(err) + + _, resp, err := evmtest.CallContractTx( + &deps, + precompile.PrecompileAddr_FunToken, + input, + deps.Sender, + ) + s.Require().NoError(err) + s.Require().Empty(resp.VmError) + + evmtest.AssertERC20BalanceEqual(s.T(), deps, erc20, deps.Sender.EthAddr, big.NewInt(69_000)) + evmtest.AssertERC20BalanceEqual(s.T(), deps, erc20, evm.EVM_MODULE_ADDRESS, big.NewInt(0)) + s.Equal(sdk.NewInt(420).String(), + deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, s.funtoken.BankDenom).Amount.String(), + ) + } +} + +func (s *FuntokenSuite) TestPrecompileLocalGas() { + deps := s.deps + randomAcc := testutil.AccAddress() + + deployResp, err := evmtest.DeployContract( + &deps, embeds.SmartContract_TestFunTokenPrecompileLocalGas, + s.funtoken.Erc20Addr.Address, ) s.Require().NoError(err) - s.Require().Empty(resp.VmError) + contractAddr := deployResp.ContractAddr - evmtest.AssertERC20BalanceEqual(s.T(), deps, erc20, deps.Sender.EthAddr, big.NewInt(69_000)) - evmtest.AssertERC20BalanceEqual(s.T(), deps, erc20, evm.EVM_MODULE_ADDRESS, big.NewInt(0)) - s.Equal(sdk.NewInt(420).String(), - deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, funtoken.BankDenom).Amount.String(), + s.T().Log("Fund sender's wallet") + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + sdk.NewCoins(sdk.NewCoin(s.funtoken.BankDenom, sdk.NewInt(1000))), + )) + + s.T().Log("Fund contract with erc20 coins") + _, err = deps.EvmKeeper.ConvertCoinToEvm( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgConvertCoinToEvm{ + Sender: deps.Sender.NibiruAddr.String(), + BankCoin: sdk.NewCoin(s.funtoken.BankDenom, sdk.NewInt(1000)), + ToEthAddr: eth.EIP55Addr{ + Address: contractAddr, + }, + }, + ) + s.Require().NoError(err) + + s.deps.ResetGasMeter() + + s.T().Log("Happy: callBankSend with default gas") + _, err = deps.EvmKeeper.CallContract( + deps.Ctx, + embeds.SmartContract_TestFunTokenPrecompileLocalGas.ABI, + deps.Sender.EthAddr, + &contractAddr, + true, + precompile.FunTokenGasLimitBankSend, + "callBankSend", + big.NewInt(1), + randomAcc.String(), + ) + s.Require().NoError(err) + + s.deps.ResetGasMeter() + + s.T().Log("Happy: callBankSend with local gas - sufficient gas amount") + _, err = deps.EvmKeeper.CallContract( + deps.Ctx, + embeds.SmartContract_TestFunTokenPrecompileLocalGas.ABI, + deps.Sender.EthAddr, + &contractAddr, + true, + precompile.FunTokenGasLimitBankSend, // gasLimit for the entire call + "callBankSendLocalGas", + big.NewInt(1), // erc20 amount + randomAcc.String(), // to + big.NewInt(int64(precompile.FunTokenGasLimitBankSend)), // customGas ) + s.Require().NoError(err) } diff --git a/x/evm/precompile/oracle.go b/x/evm/precompile/oracle.go index 987e7f75a..f5d7bb118 100644 --- a/x/evm/precompile/oracle.go +++ b/x/evm/precompile/oracle.go @@ -31,11 +31,6 @@ const ( OracleMethod_queryExchangeRate PrecompileMethod = "queryExchangeRate" ) -const ( - // OracleGasLimitQuery is a rough limit. Actual gas used for this precompile is 22_880 - OracleGasLimitQuery uint64 = 100_000 -) - // Run runs the precompiled contract func (p precompileOracle) Run( evm *vm.EVM, contract *vm.Contract, readonly bool, @@ -49,8 +44,7 @@ func (p precompileOracle) Run( } method, args, ctx := start.Method, start.Args, start.Ctx - // This handles any out of gas errors that may occur during the execution of a precompile tx or query. - // It avoids panics and returns the out of gas error so the EVM can continue gracefully. + // Resets the gas meter to parent one after precompile execution and gracefully handles "out of gas" defer ReturnToParentGasMeter(start.Ctx, contract, start.parentGasMeter, &err)() switch PrecompileMethod(method.Name) { diff --git a/x/evm/precompile/oracle_test.go b/x/evm/precompile/oracle_test.go index c68d3db74..c90b5329c 100644 --- a/x/evm/precompile/oracle_test.go +++ b/x/evm/precompile/oracle_test.go @@ -12,6 +12,8 @@ import ( "github.com/NibiruChain/nibiru/v2/x/evm/precompile" ) +const OracleGasLimitQuery = 100_000 + func (s *OracleSuite) TestOracle_FailToPackABI() { testcases := []struct { name string @@ -64,7 +66,7 @@ func (s *OracleSuite) TestOracle_HappyPath() { &precompile.PrecompileAddr_Oracle, false, input, - precompile.OracleGasLimitQuery, + OracleGasLimitQuery, ) s.NoError(err) diff --git a/x/evm/precompile/test/export.go b/x/evm/precompile/test/export.go index 435e60bfb..45a251a4d 100644 --- a/x/evm/precompile/test/export.go +++ b/x/evm/precompile/test/export.go @@ -21,6 +21,12 @@ import ( "github.com/NibiruChain/nibiru/v2/x/evm/statedb" ) +// rough gas limits for wasm execution - used in tests only +const ( + WasmGasLimitInstantiate uint64 = 1_000_000 + WasmGasLimitExecute uint64 = 10_000_000 +) + // SetupWasmContracts stores all Wasm bytecode and has the "deps.Sender" // instantiate each Wasm contract using the precompile. func SetupWasmContracts(deps *evmtest.TestDeps, s *suite.Suite) ( @@ -76,7 +82,7 @@ func SetupWasmContracts(deps *evmtest.TestDeps, s *suite.Suite) ( &precompile.PrecompileAddr_Wasm, true, input, - precompile.WasmGasLimitInstantiate, + WasmGasLimitInstantiate, ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) @@ -196,7 +202,7 @@ func AssertWasmCounterState( &precompile.PrecompileAddr_Wasm, true, input, - precompile.WasmGasLimitInstantiate, + WasmGasLimitInstantiate, ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) @@ -317,13 +323,15 @@ func IncrementWasmCounterWithExecuteMulti( ) s.Require().NoError(err) + deps.ResetGasMeter() + ethTxResp, evmObj, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, - precompile.WasmGasLimitExecute, + WasmGasLimitExecute, ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) diff --git a/x/evm/precompile/wasm.go b/x/evm/precompile/wasm.go index 3982c4efc..a8fe80962 100644 --- a/x/evm/precompile/wasm.go +++ b/x/evm/precompile/wasm.go @@ -29,14 +29,6 @@ const ( WasmMethod_queryRaw PrecompileMethod = "queryRaw" ) -const ( - // rough gas limits for wasm execution - - WasmGasLimitInstantiate uint64 = 10_000_000 - WasmGasLimitQuery uint64 = 10_000_000 - WasmGasLimitExecute uint64 = 10_000_000 -) - // Run runs the precompiled contract func (p precompileWasm) Run( evm *vm.EVM, contract *vm.Contract, readonly bool, @@ -50,8 +42,7 @@ func (p precompileWasm) Run( } method := start.Method - // This handles any out of gas errors that may occur during the execution of a precompile tx or query. - // It avoids panics and returns the out of gas error so the EVM can continue gracefully. + // Resets the gas meter to parent one after precompile execution and gracefully handles "out of gas" defer ReturnToParentGasMeter(start.Ctx, contract, start.parentGasMeter, &err)() switch PrecompileMethod(method.Name) { diff --git a/x/evm/precompile/wasm_test.go b/x/evm/precompile/wasm_test.go index effc2f6c5..f122625de 100644 --- a/x/evm/precompile/wasm_test.go +++ b/x/evm/precompile/wasm_test.go @@ -18,6 +18,12 @@ import ( "github.com/stretchr/testify/suite" ) +// rough gas limits for wasm execution - used in tests only +const ( + WasmGasLimitQuery uint64 = 200_000 + WasmGasLimitExecute uint64 = 1_000_000 +) + type WasmSuite struct { suite.Suite } @@ -58,7 +64,7 @@ func (s *WasmSuite) TestExecuteHappy() { &precompile.PrecompileAddr_Wasm, true, input, - precompile.WasmGasLimitExecute, + WasmGasLimitExecute, ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) @@ -92,7 +98,7 @@ func (s *WasmSuite) TestExecuteHappy() { &precompile.PrecompileAddr_Wasm, true, input, - precompile.WasmGasLimitExecute, + WasmGasLimitExecute, ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) @@ -146,13 +152,15 @@ func (s *WasmSuite) assertWasmCounterStateRaw( ) s.Require().NoError(err) + deps.ResetGasMeter() + ethTxResp, _, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, - precompile.WasmGasLimitQuery, + WasmGasLimitQuery, ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) @@ -327,7 +335,7 @@ func (s *WasmSuite) TestSadArgsExecute() { &precompile.PrecompileAddr_Wasm, true, input, - precompile.WasmGasLimitExecute, + WasmGasLimitExecute, ) s.Require().ErrorContains(err, tc.wantError, "ethTxResp %v", ethTxResp) }) From f3e3b87cc82c86b984118b736ca85cbc23e2f926 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Mon, 28 Oct 2024 20:37:26 +0400 Subject: [PATCH 4/5] fix: increased funtoken precompile gas limit in favor to heavy user contracts --- x/evm/precompile/funtoken.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go index d1085700b..a4062a3bf 100644 --- a/x/evm/precompile/funtoken.go +++ b/x/evm/precompile/funtoken.go @@ -37,7 +37,7 @@ const ( // ~60_000 gas for either mint or burn // 3. send from module to account: // ~65_000 gas (bank send) - FunTokenGasLimitBankSend uint64 = 200_000 + FunTokenGasLimitBankSend uint64 = 400_000 ) func (p precompileFunToken) Address() gethcommon.Address { From 67de7750dbc97c7dc59522c90b151385428e8043 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Tue, 29 Oct 2024 19:17:42 +0400 Subject: [PATCH 5/5] chore: moved evm grpc calls back to msg_server.go --- x/evm/keeper/call_contract.go | 155 ++++++++++++++ x/evm/keeper/funtoken_from_coin.go | 74 ------- x/evm/keeper/funtoken_from_erc20.go | 93 --------- x/evm/keeper/msg_server.go | 309 +++++++++++++++------------- 4 files changed, 323 insertions(+), 308 deletions(-) create mode 100644 x/evm/keeper/call_contract.go diff --git a/x/evm/keeper/call_contract.go b/x/evm/keeper/call_contract.go new file mode 100644 index 000000000..1a89eaf51 --- /dev/null +++ b/x/evm/keeper/call_contract.go @@ -0,0 +1,155 @@ +package keeper + +import ( + "fmt" + "math/big" + "strings" + + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + gethabi "github.com/ethereum/go-ethereum/accounts/abi" + gethcommon "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + + "github.com/NibiruChain/nibiru/v2/x/evm" +) + +// CallContract invokes a smart contract on the method specified by [methodName] +// using the given [args]. +// +// Parameters: +// - ctx: The SDK context for the transaction. +// - abi: The ABI (Application Binary Interface) of the smart contract. +// - fromAcc: The Ethereum address of the account initiating the contract call. +// - contract: Pointer to the Ethereum address of the contract to be called. +// - commit: Boolean flag indicating whether to commit the transaction (true) or simulate it (false). +// - methodName: The name of the contract method to be called. +// - args: Variadic parameter for the arguments to be passed to the contract method. +// +// Note: This function handles both contract method calls and simulations, +// depending on the 'commit' parameter. +func (k Keeper) CallContract( + ctx sdk.Context, + abi *gethabi.ABI, + fromAcc gethcommon.Address, + contract *gethcommon.Address, + commit bool, + gasLimit uint64, + methodName string, + args ...any, +) (evmResp *evm.MsgEthereumTxResponse, err error) { + contractInput, err := abi.Pack(methodName, args...) + if err != nil { + return nil, fmt.Errorf("failed to pack ABI args: %w", err) + } + evmResp, _, err = k.CallContractWithInput(ctx, fromAcc, contract, commit, contractInput, gasLimit) + return evmResp, err +} + +// CallContractWithInput invokes a smart contract with the given [contractInput] +// or deploys a new contract. +// +// Parameters: +// - ctx: The SDK context for the transaction. +// - fromAcc: The Ethereum address of the account initiating the contract call. +// - contract: Pointer to the Ethereum address of the contract. Nil if new +// contract is deployed. +// - commit: Boolean flag indicating whether to commit the transaction (true) +// or simulate it (false). +// - contractInput: Hexadecimal-encoded bytes use as input data to the call. +// +// Note: This function handles both contract method calls and simulations, +// depending on the 'commit' parameter. It uses a default gas limit. +func (k Keeper) CallContractWithInput( + ctx sdk.Context, + fromAcc gethcommon.Address, + contract *gethcommon.Address, + commit bool, + contractInput []byte, + gasLimit uint64, +) (evmResp *evm.MsgEthereumTxResponse, evmObj *vm.EVM, err error) { + // This is a `defer` pattern to add behavior that runs in the case that the + // error is non-nil, creating a concise way to add extra information. + defer func() { + if err != nil { + err = fmt.Errorf("CallContractError: %w", err) + } + }() + nonce := k.GetAccNonce(ctx, fromAcc) + + unusedBigInt := big.NewInt(0) + evmMsg := gethcore.NewMessage( + fromAcc, + contract, + nonce, + unusedBigInt, // amount + gasLimit, + unusedBigInt, // gasFeeCap + unusedBigInt, // gasTipCap + unusedBigInt, // gasPrice + contractInput, + gethcore.AccessList{}, + !commit, // isFake + ) + + // Apply EVM message + evmCfg, err := k.GetEVMConfig( + ctx, + sdk.ConsAddress(ctx.BlockHeader().ProposerAddress), + k.EthChainID(ctx), + ) + if err != nil { + err = errors.Wrapf(err, "failed to load EVM config") + return + } + + // Generating TxConfig with an empty tx hash as there is no actual eth tx + // sent by a user + txConfig := k.TxConfig(ctx, gethcommon.BigToHash(big.NewInt(0))) + + // Using tmp context to not modify the state in case of evm revert + tmpCtx, commitCtx := ctx.CacheContext() + + evmResp, evmObj, err = k.ApplyEvmMsg( + tmpCtx, evmMsg, evm.NewNoOpTracer(), commit, evmCfg, txConfig, true, + ) + if err != nil { + // We don't know the actual gas used, so consuming the gas limit + k.ResetGasMeterAndConsumeGas(ctx, gasLimit) + err = errors.Wrap(err, "failed to apply ethereum core message") + return + } + if evmResp.Failed() { + k.ResetGasMeterAndConsumeGas(ctx, evmResp.GasUsed) + if !strings.Contains(evmResp.VmError, vm.ErrOutOfGas.Error()) { + if evmResp.VmError == vm.ErrExecutionReverted.Error() { + err = fmt.Errorf("VMError: %w", evm.NewExecErrorWithReason(evmResp.Ret)) + return + } + err = fmt.Errorf("VMError: %s", evmResp.VmError) + return + } + err = fmt.Errorf("gas required exceeds allowance (%d)", gasLimit) + return + } else { + // Success, committing the state to ctx + if commit { + commitCtx() + totalGasUsed, err := k.AddToBlockGasUsed(ctx, evmResp.GasUsed) + if err != nil { + k.ResetGasMeterAndConsumeGas(ctx, ctx.GasMeter().Limit()) + return nil, nil, errors.Wrap(err, "error adding transient gas used to block") + } + k.ResetGasMeterAndConsumeGas(ctx, totalGasUsed) + k.updateBlockBloom(ctx, evmResp, uint64(txConfig.LogIndex)) + err = k.EmitEthereumTxEvents(ctx, contract, gethcore.LegacyTxType, evmMsg, evmResp) + if err != nil { + return nil, nil, errors.Wrap(err, "error emitting ethereum tx events") + } + blockTxIdx := uint64(txConfig.TxIndex) + 1 + k.EvmState.BlockTxIndex.Set(ctx, blockTxIdx) + } + return evmResp, evmObj, nil + } +} diff --git a/x/evm/keeper/funtoken_from_coin.go b/x/evm/keeper/funtoken_from_coin.go index f2110dc98..feee20b43 100644 --- a/x/evm/keeper/funtoken_from_coin.go +++ b/x/evm/keeper/funtoken_from_coin.go @@ -1,7 +1,6 @@ package keeper import ( - "context" "fmt" "cosmossdk.io/errors" @@ -88,76 +87,3 @@ func (k *Keeper) deployERC20ForBankCoin( return erc20Addr, nil } - -// ConvertCoinToEvm Sends a coin with a valid "FunToken" mapping to the -// given recipient address ("to_eth_addr") in the corresponding ERC20 -// representation. -func (k *Keeper) ConvertCoinToEvm( - goCtx context.Context, msg *evm.MsgConvertCoinToEvm, -) (resp *evm.MsgConvertCoinToEvmResponse, err error) { - ctx := sdk.UnwrapSDKContext(goCtx) - - sender := sdk.MustAccAddressFromBech32(msg.Sender) - - funTokens := k.FunTokens.Collect(ctx, k.FunTokens.Indexes.BankDenom.ExactMatch(ctx, msg.BankCoin.Denom)) - if len(funTokens) == 0 { - return nil, fmt.Errorf("funtoken for bank denom \"%s\" does not exist", msg.BankCoin.Denom) - } - if len(funTokens) > 1 { - return nil, fmt.Errorf("multiple funtokens for bank denom \"%s\" found", msg.BankCoin.Denom) - } - - fungibleTokenMapping := funTokens[0] - - if fungibleTokenMapping.IsMadeFromCoin { - return k.convertCoinNativeCoin(ctx, sender, msg.ToEthAddr.Address, msg.BankCoin, fungibleTokenMapping) - } else { - return k.convertCoinNativeERC20(ctx, sender, msg.ToEthAddr.Address, msg.BankCoin, fungibleTokenMapping) - } -} - -// Converts a native coin to an ERC20 token. -// EVM module owns the ERC-20 contract and can mint the ERC-20 tokens. -func (k Keeper) convertCoinNativeCoin( - ctx sdk.Context, - sender sdk.AccAddress, - recipient gethcommon.Address, - coin sdk.Coin, - funTokenMapping evm.FunToken, -) (*evm.MsgConvertCoinToEvmResponse, error) { - // Step 1: Escrow bank coins with EVM module account - err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, evm.ModuleName, sdk.NewCoins(coin)) - if err != nil { - return nil, errors.Wrap(err, "failed to send coins to module account") - } - - erc20Addr := funTokenMapping.Erc20Addr.Address - - // Step 2: mint ERC-20 tokens for recipient - evmResp, err := k.CallContract( - ctx, - embeds.SmartContract_ERC20Minter.ABI, - evm.EVM_MODULE_ADDRESS, - &erc20Addr, - true, - Erc20GasLimitExecute, - "mint", - recipient, - coin.Amount.BigInt(), - ) - if err != nil { - return nil, err - } - if evmResp.Failed() { - return nil, - fmt.Errorf("failed to mint erc-20 tokens of contract %s", erc20Addr.String()) - } - _ = ctx.EventManager().EmitTypedEvent(&evm.EventConvertCoinToEvm{ - Sender: sender.String(), - Erc20ContractAddress: erc20Addr.String(), - ToEthAddr: recipient.String(), - BankCoin: coin, - }) - - return &evm.MsgConvertCoinToEvmResponse{}, nil -} diff --git a/x/evm/keeper/funtoken_from_erc20.go b/x/evm/keeper/funtoken_from_erc20.go index 997d986e3..e10f0cca8 100644 --- a/x/evm/keeper/funtoken_from_erc20.go +++ b/x/evm/keeper/funtoken_from_erc20.go @@ -5,7 +5,6 @@ import ( "fmt" "math/big" - "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" bank "github.com/cosmos/cosmos-sdk/x/bank/types" gethcommon "github.com/ethereum/go-ethereum/common" @@ -203,95 +202,3 @@ func (erc20Info ERC20Metadata) ToBankMetadata( Symbol: symbol, } } - -// Converts a coin that was originally an ERC20 token, and that was converted to a bank coin, back to an ERC20 token. -// EVM module does not own the ERC-20 contract and cannot mint the ERC-20 tokens. -// EVM module has escrowed tokens in the first conversion from ERC-20 to bank coin. -func (k Keeper) convertCoinNativeERC20( - ctx sdk.Context, - sender sdk.AccAddress, - recipient gethcommon.Address, - coin sdk.Coin, - funTokenMapping evm.FunToken, -) (*evm.MsgConvertCoinToEvmResponse, error) { - erc20Addr := funTokenMapping.Erc20Addr.Address - - recipientBalanceBefore, err := k.ERC20().BalanceOf(erc20Addr, recipient, ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to retrieve balance") - } - if recipientBalanceBefore == nil { - return nil, fmt.Errorf("failed to retrieve balance, balance is nil") - } - - // Escrow Coins on module account - if err := k.bankKeeper.SendCoinsFromAccountToModule( - ctx, - sender, - evm.ModuleName, - sdk.NewCoins(coin), - ); err != nil { - return nil, errors.Wrap(err, "failed to escrow coins") - } - - // verify that the EVM module account has enough escrowed ERC-20 to transfer - // should never fail, because the coins were minted from the escrowed tokens, but check just in case - evmModuleBalance, err := k.ERC20().BalanceOf( - erc20Addr, - evm.EVM_MODULE_ADDRESS, - ctx, - ) - if err != nil { - return nil, errors.Wrap(err, "failed to retrieve balance") - } - if evmModuleBalance == nil { - return nil, fmt.Errorf("failed to retrieve balance, balance is nil") - } - if evmModuleBalance.Cmp(coin.Amount.BigInt()) < 0 { - return nil, fmt.Errorf("insufficient balance in EVM module account") - } - - // unescrow ERC-20 tokens from EVM module address - res, err := k.ERC20().Transfer( - erc20Addr, - evm.EVM_MODULE_ADDRESS, - recipient, - coin.Amount.BigInt(), - ctx, - ) - if err != nil { - return nil, errors.Wrap(err, "failed to transfer ERC20 tokens") - } - if !res { - return nil, fmt.Errorf("failed to transfer ERC20 tokens") - } - - // Check expected Receiver balance after transfer execution - recipientBalanceAfter, err := k.ERC20().BalanceOf(erc20Addr, recipient, ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to retrieve balance") - } - if recipientBalanceAfter == nil { - return nil, fmt.Errorf("failed to retrieve balance, balance is nil") - } - - expectedFinalBalance := big.NewInt(0).Add(recipientBalanceBefore, coin.Amount.BigInt()) - if r := recipientBalanceAfter.Cmp(expectedFinalBalance); r != 0 { - return nil, fmt.Errorf("expected balance after transfer to be %s, got %s", expectedFinalBalance, recipientBalanceAfter) - } - - // Burn escrowed Coins - err = k.bankKeeper.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(coin)) - if err != nil { - return nil, errors.Wrap(err, "failed to burn coins") - } - - _ = ctx.EventManager().EmitTypedEvent(&evm.EventConvertCoinToEvm{ - Sender: sender.String(), - Erc20ContractAddress: funTokenMapping.Erc20Addr.String(), - ToEthAddr: recipient.String(), - BankCoin: coin, - }) - - return &evm.MsgConvertCoinToEvmResponse{}, nil -} diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index b55889ec2..d1d368de1 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -7,14 +7,12 @@ import ( "fmt" "math/big" "strconv" - "strings" "cosmossdk.io/errors" "cosmossdk.io/math" tmbytes "github.com/cometbft/cometbft/libs/bytes" tmtypes "github.com/cometbft/cometbft/types" sdk "github.com/cosmos/cosmos-sdk/types" - gethabi "github.com/ethereum/go-ethereum/accounts/abi" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" gethcore "github.com/ethereum/go-ethereum/core/types" @@ -22,6 +20,8 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" + "github.com/NibiruChain/nibiru/v2/x/evm/embeds" + "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/x/evm" "github.com/NibiruChain/nibiru/v2/x/evm/statedb" @@ -272,6 +272,7 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, sender := vm.AccountRef(msg.From()) contractCreation := msg.To() == nil + intrinsicGas, err := k.GetEthIntrinsicGas(ctx, msg, evmConfig.ChainConfig, contractCreation) if err != nil { // should have already been checked on Ante Handler @@ -474,6 +475,171 @@ func (k Keeper) FeeForCreateFunToken(ctx sdk.Context) sdk.Coins { return sdk.NewCoins(sdk.NewCoin(evm.EVMBankDenom, evmParams.CreateFuntokenFee)) } +// ConvertCoinToEvm Sends a coin with a valid "FunToken" mapping to the +// given recipient address ("to_eth_addr") in the corresponding ERC20 +// representation. +func (k *Keeper) ConvertCoinToEvm( + goCtx context.Context, msg *evm.MsgConvertCoinToEvm, +) (resp *evm.MsgConvertCoinToEvmResponse, err error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + sender := sdk.MustAccAddressFromBech32(msg.Sender) + + funTokens := k.FunTokens.Collect(ctx, k.FunTokens.Indexes.BankDenom.ExactMatch(ctx, msg.BankCoin.Denom)) + if len(funTokens) == 0 { + return nil, fmt.Errorf("funtoken for bank denom \"%s\" does not exist", msg.BankCoin.Denom) + } + if len(funTokens) > 1 { + return nil, fmt.Errorf("multiple funtokens for bank denom \"%s\" found", msg.BankCoin.Denom) + } + + fungibleTokenMapping := funTokens[0] + + if fungibleTokenMapping.IsMadeFromCoin { + return k.convertCoinNativeCoin(ctx, sender, msg.ToEthAddr.Address, msg.BankCoin, fungibleTokenMapping) + } else { + return k.convertCoinNativeERC20(ctx, sender, msg.ToEthAddr.Address, msg.BankCoin, fungibleTokenMapping) + } +} + +// Converts a native coin to an ERC20 token. +// EVM module owns the ERC-20 contract and can mint the ERC-20 tokens. +func (k Keeper) convertCoinNativeCoin( + ctx sdk.Context, + sender sdk.AccAddress, + recipient gethcommon.Address, + coin sdk.Coin, + funTokenMapping evm.FunToken, +) (*evm.MsgConvertCoinToEvmResponse, error) { + // Step 1: Escrow bank coins with EVM module account + err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, evm.ModuleName, sdk.NewCoins(coin)) + if err != nil { + return nil, errors.Wrap(err, "failed to send coins to module account") + } + + erc20Addr := funTokenMapping.Erc20Addr.Address + + // Step 2: mint ERC-20 tokens for recipient + evmResp, err := k.CallContract( + ctx, + embeds.SmartContract_ERC20Minter.ABI, + evm.EVM_MODULE_ADDRESS, + &erc20Addr, + true, + Erc20GasLimitExecute, + "mint", + recipient, + coin.Amount.BigInt(), + ) + if err != nil { + return nil, err + } + if evmResp.Failed() { + return nil, + fmt.Errorf("failed to mint erc-20 tokens of contract %s", erc20Addr.String()) + } + _ = ctx.EventManager().EmitTypedEvent(&evm.EventConvertCoinToEvm{ + Sender: sender.String(), + Erc20ContractAddress: erc20Addr.String(), + ToEthAddr: recipient.String(), + BankCoin: coin, + }) + + return &evm.MsgConvertCoinToEvmResponse{}, nil +} + +// Converts a coin that was originally an ERC20 token, and that was converted to a bank coin, back to an ERC20 token. +// EVM module does not own the ERC-20 contract and cannot mint the ERC-20 tokens. +// EVM module has escrowed tokens in the first conversion from ERC-20 to bank coin. +func (k Keeper) convertCoinNativeERC20( + ctx sdk.Context, + sender sdk.AccAddress, + recipient gethcommon.Address, + coin sdk.Coin, + funTokenMapping evm.FunToken, +) (*evm.MsgConvertCoinToEvmResponse, error) { + erc20Addr := funTokenMapping.Erc20Addr.Address + + recipientBalanceBefore, err := k.ERC20().BalanceOf(erc20Addr, recipient, ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve balance") + } + if recipientBalanceBefore == nil { + return nil, fmt.Errorf("failed to retrieve balance, balance is nil") + } + + // Escrow Coins on module account + if err := k.bankKeeper.SendCoinsFromAccountToModule( + ctx, + sender, + evm.ModuleName, + sdk.NewCoins(coin), + ); err != nil { + return nil, errors.Wrap(err, "failed to escrow coins") + } + + // verify that the EVM module account has enough escrowed ERC-20 to transfer + // should never fail, because the coins were minted from the escrowed tokens, but check just in case + evmModuleBalance, err := k.ERC20().BalanceOf( + erc20Addr, + evm.EVM_MODULE_ADDRESS, + ctx, + ) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve balance") + } + if evmModuleBalance == nil { + return nil, fmt.Errorf("failed to retrieve balance, balance is nil") + } + if evmModuleBalance.Cmp(coin.Amount.BigInt()) < 0 { + return nil, fmt.Errorf("insufficient balance in EVM module account") + } + + // unescrow ERC-20 tokens from EVM module address + res, err := k.ERC20().Transfer( + erc20Addr, + evm.EVM_MODULE_ADDRESS, + recipient, + coin.Amount.BigInt(), + ctx, + ) + if err != nil { + return nil, errors.Wrap(err, "failed to transfer ERC20 tokens") + } + if !res { + return nil, fmt.Errorf("failed to transfer ERC20 tokens") + } + + // Check expected Receiver balance after transfer execution + recipientBalanceAfter, err := k.ERC20().BalanceOf(erc20Addr, recipient, ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve balance") + } + if recipientBalanceAfter == nil { + return nil, fmt.Errorf("failed to retrieve balance, balance is nil") + } + + expectedFinalBalance := big.NewInt(0).Add(recipientBalanceBefore, coin.Amount.BigInt()) + if r := recipientBalanceAfter.Cmp(expectedFinalBalance); r != 0 { + return nil, fmt.Errorf("expected balance after transfer to be %s, got %s", expectedFinalBalance, recipientBalanceAfter) + } + + // Burn escrowed Coins + err = k.bankKeeper.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(coin)) + if err != nil { + return nil, errors.Wrap(err, "failed to burn coins") + } + + _ = ctx.EventManager().EmitTypedEvent(&evm.EventConvertCoinToEvm{ + Sender: sender.String(), + Erc20ContractAddress: funTokenMapping.Erc20Addr.String(), + ToEthAddr: recipient.String(), + BankCoin: coin, + }) + + return &evm.MsgConvertCoinToEvmResponse{}, nil +} + // EmitEthereumTxEvents emits all types of EVM events applicable to a particular execution case func (k *Keeper) EmitEthereumTxEvents( ctx sdk.Context, @@ -561,142 +727,3 @@ func (k *Keeper) updateBlockBloom( k.EvmState.BlockLogSize.Set(ctx, blockLogSize) } } - -// CallContract invokes a smart contract on the method specified by [methodName] -// using the given [args]. -// -// Parameters: -// - ctx: The SDK context for the transaction. -// - abi: The ABI (Application Binary Interface) of the smart contract. -// - fromAcc: The Ethereum address of the account initiating the contract call. -// - contract: Pointer to the Ethereum address of the contract to be called. -// - commit: Boolean flag indicating whether to commit the transaction (true) or simulate it (false). -// - methodName: The name of the contract method to be called. -// - args: Variadic parameter for the arguments to be passed to the contract method. -// -// Note: This function handles both contract method calls and simulations, -// depending on the 'commit' parameter. -func (k Keeper) CallContract( - ctx sdk.Context, - abi *gethabi.ABI, - fromAcc gethcommon.Address, - contract *gethcommon.Address, - commit bool, - gasLimit uint64, - methodName string, - args ...any, -) (evmResp *evm.MsgEthereumTxResponse, err error) { - contractInput, err := abi.Pack(methodName, args...) - if err != nil { - return nil, fmt.Errorf("failed to pack ABI args: %w", err) - } - evmResp, _, err = k.CallContractWithInput(ctx, fromAcc, contract, commit, contractInput, gasLimit) - return evmResp, err -} - -// CallContractWithInput invokes a smart contract with the given [contractInput] -// or deploys a new contract. -// -// Parameters: -// - ctx: The SDK context for the transaction. -// - fromAcc: The Ethereum address of the account initiating the contract call. -// - contract: Pointer to the Ethereum address of the contract. Nil if new -// contract is deployed. -// - commit: Boolean flag indicating whether to commit the transaction (true) -// or simulate it (false). -// - contractInput: Hexadecimal-encoded bytes use as input data to the call. -// -// Note: This function handles both contract method calls and simulations, -// depending on the 'commit' parameter. It uses a default gas limit. -func (k Keeper) CallContractWithInput( - ctx sdk.Context, - fromAcc gethcommon.Address, - contract *gethcommon.Address, - commit bool, - contractInput []byte, - gasLimit uint64, -) (evmResp *evm.MsgEthereumTxResponse, evmObj *vm.EVM, err error) { - // This is a `defer` pattern to add behavior that runs in the case that the - // error is non-nil, creating a concise way to add extra information. - defer func() { - if err != nil { - err = fmt.Errorf("CallContractError: %w", err) - } - }() - nonce := k.GetAccNonce(ctx, fromAcc) - - unusedBigInt := big.NewInt(0) - evmMsg := gethcore.NewMessage( - fromAcc, - contract, - nonce, - unusedBigInt, // amount - gasLimit, - unusedBigInt, // gasFeeCap - unusedBigInt, // gasTipCap - unusedBigInt, // gasPrice - contractInput, - gethcore.AccessList{}, - !commit, // isFake - ) - - // Apply EVM message - evmCfg, err := k.GetEVMConfig( - ctx, - sdk.ConsAddress(ctx.BlockHeader().ProposerAddress), - k.EthChainID(ctx), - ) - if err != nil { - err = errors.Wrapf(err, "failed to load EVM config") - return - } - - // Generating TxConfig with an empty tx hash as there is no actual eth tx - // sent by a user - txConfig := k.TxConfig(ctx, gethcommon.BigToHash(big.NewInt(0))) - - // Using tmp context to not modify the state in case of evm revert - tmpCtx, commitCtx := ctx.CacheContext() - - evmResp, evmObj, err = k.ApplyEvmMsg( - tmpCtx, evmMsg, evm.NewNoOpTracer(), commit, evmCfg, txConfig, true, - ) - if err != nil { - // We don't know the actual gas used, so consuming the gas limit - k.ResetGasMeterAndConsumeGas(ctx, gasLimit) - err = errors.Wrap(err, "failed to apply ethereum core message") - return - } - if evmResp.Failed() { - k.ResetGasMeterAndConsumeGas(ctx, evmResp.GasUsed) - if !strings.Contains(evmResp.VmError, vm.ErrOutOfGas.Error()) { - if evmResp.VmError == vm.ErrExecutionReverted.Error() { - err = fmt.Errorf("VMError: %w", evm.NewExecErrorWithReason(evmResp.Ret)) - return - } - err = fmt.Errorf("VMError: %s", evmResp.VmError) - return - } - err = fmt.Errorf("gas required exceeds allowance (%d)", gasLimit) - return - } else { - // Success, committing the state to ctx - if commit { - commitCtx() - totalGasUsed, err := k.AddToBlockGasUsed(ctx, evmResp.GasUsed) - if err != nil { - k.ResetGasMeterAndConsumeGas(ctx, ctx.GasMeter().Limit()) - return nil, nil, errors.Wrap(err, "error adding transient gas used to block") - } - k.ResetGasMeterAndConsumeGas(ctx, totalGasUsed) - k.updateBlockBloom(ctx, evmResp, uint64(txConfig.LogIndex)) - err = k.EmitEthereumTxEvents(ctx, contract, gethcore.LegacyTxType, evmMsg, evmResp) - if err != nil { - return nil, nil, errors.Wrap(err, "error emitting ethereum tx events") - } - blockTxIdx := uint64(txConfig.TxIndex) + 1 - k.EvmState.BlockTxIndex.Set(ctx, blockTxIdx) - } - return evmResp, evmObj, nil - } -}