diff --git a/CHANGELOG.md b/CHANGELOG.md index 426c9e18d..53514b6fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,8 +88,9 @@ needed to include double quotes around the hexadecimal string. - [#2173](https://github.com/NibiruChain/nibiru/pull/2173) - fix(evm): clear `StateDB` between calls - [#2177](https://github.com/NibiruChain/nibiru/pull/2177) - fix(cmd): Continue from #2127 and unwire vesting flags and logic from genaccounts.go - [#2176](https://github.com/NibiruChain/nibiru/pull/2176) - tests(evm): add dirty state tests from code4rena audit +- [#2180](https://github.com/NibiruChain/nibiru/pull/2180) - fix(evm): apply gas consumption across the entire EVM codebase at `CallContractWithInput` - [#2183](https://github.com/NibiruChain/nibiru/pull/2183) - fix(evm): bank keeper extension gas meter type - +- #### Nibiru EVM | Before Audit 2 - 2024-12-06 The codebase went through a third-party [Code4rena diff --git a/x/evm/keeper/call_contract.go b/x/evm/keeper/call_contract.go index f9fdc2c8b..032d30bd7 100644 --- a/x/evm/keeper/call_contract.go +++ b/x/evm/keeper/call_contract.go @@ -39,7 +39,7 @@ func (k Keeper) CallContractWithInput( ) (evmResp *evm.MsgEthereumTxResponse, 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 HandleOutOfGasPanic(&err, "CallContractError") + defer HandleOutOfGasPanic(&err, "CallContractError")() nonce := k.GetAccNonce(ctx, fromAcc) unusedBigInt := big.NewInt(0) @@ -61,11 +61,13 @@ func (k Keeper) CallContractWithInput( // sent by a user txConfig := k.TxConfig(ctx, gethcommon.BigToHash(big.NewInt(0))) evmResp, err = k.ApplyEvmMsg( - ctx, evmMsg, evmObj, evm.NewNoOpTracer(), commit, txConfig.TxHash, true, + ctx, evmMsg, evmObj, evm.NewNoOpTracer(), commit, txConfig.TxHash, ) + if evmResp != nil { + ctx.GasMeter().ConsumeGas(evmResp.GasUsed, "CallContractWithInput") + } if err != nil { - err = errors.Wrap(err, "failed to apply ethereum core message") - return + return nil, errors.Wrap(err, "failed to apply ethereum core message") } if evmResp.Failed() { diff --git a/x/evm/keeper/erc20.go b/x/evm/keeper/erc20.go index f26bee952..3851e06dd 100644 --- a/x/evm/keeper/erc20.go +++ b/x/evm/keeper/erc20.go @@ -196,7 +196,7 @@ func (e erc20Calls) loadERC20String( if err != nil { return out, err } - res, err := e.Keeper.CallContractWithInput( + evmResp, err := e.Keeper.CallContractWithInput( ctx, evmObj, evm.EVM_MODULE_ADDRESS, @@ -211,13 +211,13 @@ func (e erc20Calls) loadERC20String( erc20Val := new(ERC20String) if err := erc20Abi.UnpackIntoInterface( - erc20Val, methodName, res.Ret, + erc20Val, methodName, evmResp.Ret, ); err == nil { return erc20Val.Value, err } erc20Bytes32Val := new(ERC20Bytes32) - if err := erc20Abi.UnpackIntoInterface(erc20Bytes32Val, methodName, res.Ret); err == nil { + if err := erc20Abi.UnpackIntoInterface(erc20Bytes32Val, methodName, evmResp.Ret); err == nil { return bytes32ToString(erc20Bytes32Val.Value), nil } @@ -239,7 +239,7 @@ func (e erc20Calls) loadERC20Uint8( if err != nil { return out, err } - res, err := e.Keeper.CallContractWithInput( + evmResp, err := e.Keeper.CallContractWithInput( ctx, evmObj, evm.EVM_MODULE_ADDRESS, @@ -254,14 +254,14 @@ func (e erc20Calls) loadERC20Uint8( erc20Val := new(ERC20Uint8) if err := erc20Abi.UnpackIntoInterface( - erc20Val, methodName, res.Ret, + erc20Val, methodName, evmResp.Ret, ); err == nil { return erc20Val.Value, err } erc20Uint256Val := new(ERC20BigInt) if err := erc20Abi.UnpackIntoInterface( - erc20Uint256Val, methodName, res.Ret, + erc20Uint256Val, methodName, evmResp.Ret, ); err == nil { // We can safely cast to uint8 because it's nonsense for decimals to be larger than 255 return uint8(erc20Uint256Val.Value.Uint64()), err diff --git a/x/evm/keeper/funtoken_from_coin.go b/x/evm/keeper/funtoken_from_coin.go index 294d72964..6cb4977d5 100644 --- a/x/evm/keeper/funtoken_from_coin.go +++ b/x/evm/keeper/funtoken_from_coin.go @@ -102,7 +102,7 @@ func (k *Keeper) deployERC20ForBankCoin( k.Bank.StateDB = nil }() evmObj := k.NewEVM(ctx, evmMsg, evmCfg, nil /*tracer*/, stateDB) - evmResp, err := k.CallContractWithInput( + _, err = k.CallContractWithInput( ctx, evmObj, evm.EVM_MODULE_ADDRESS, nil, true /*commit*/, input, Erc20GasLimitDeploy, ) if err != nil { @@ -114,7 +114,5 @@ func (k *Keeper) deployERC20ForBankCoin( return gethcommon.Address{}, errors.Wrap(err, "failed to commit stateDB") } - ctx.GasMeter().ConsumeGas(evmResp.GasUsed, "deploy erc20 funtoken contract") - return erc20Addr, nil } diff --git a/x/evm/keeper/funtoken_from_coin_test.go b/x/evm/keeper/funtoken_from_coin_test.go index 432d3ce5b..32563b002 100644 --- a/x/evm/keeper/funtoken_from_coin_test.go +++ b/x/evm/keeper/funtoken_from_coin_test.go @@ -82,6 +82,7 @@ func (s *FunTokenFromCoinSuite) TestCreateFunTokenFromCoin() { }) s.Run("happy: CreateFunToken for the bank coin", func() { + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) s.Require().NoError(testapp.FundAccount( deps.App.BankKeeper, deps.Ctx, @@ -97,6 +98,7 @@ func (s *FunTokenFromCoinSuite) TestCreateFunTokenFromCoin() { }, ) s.Require().NoError(err) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) s.Equal( createFuntokenResp.FuntokenMapping, @@ -167,6 +169,7 @@ func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() { funToken := s.fundAndCreateFunToken(deps, 100) s.T().Log("Convert bank coin to erc-20") + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) _, err := deps.EvmKeeper.ConvertCoinToEvm( sdk.WrapSDKContext(deps.Ctx), &evm.MsgConvertCoinToEvm{ @@ -178,6 +181,7 @@ func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() { }, ) s.Require().NoError(err) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) s.T().Log("Check typed event") testutil.RequireContainsTypedEvent( @@ -226,6 +230,7 @@ func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() { deps.Sender.NibiruAddr.String(), ) s.Require().NoError(err) + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) evmObj, _ = deps.NewEVM() _, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, @@ -237,6 +242,7 @@ func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() { evmtest.FunTokenGasLimitSendToEvm, ) s.Require().NoError(err) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) // Check 1: module balance moduleBalance = deps.App.BankKeeper.GetBalance(deps.Ctx, authtypes.NewModuleAddress(evm.ModuleName), evm.EVMBankDenom) @@ -252,6 +258,7 @@ func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() { s.Require().Equal("0", balance.String()) s.T().Log("sad: Convert more erc-20 to back to bank coin, insufficient funds") + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) evmObj, _ = deps.NewEVM() _, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, @@ -263,6 +270,7 @@ func (s *FunTokenFromCoinSuite) TestConvertCoinToEvmAndBack() { evmtest.FunTokenGasLimitSendToEvm, ) s.Require().ErrorContains(err, "transfer amount exceeds balance") + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) } // TestNativeSendThenPrecompileSend tests a race condition where the state DB @@ -362,6 +370,7 @@ func (s *FunTokenFromCoinSuite) TestNativeSendThenPrecompileSend() { newSendAmtSendToBank, /*amount*/ ) s.Require().NoError(err) + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) evmObj, _ = deps.NewEVM() evmResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, @@ -373,6 +382,9 @@ func (s *FunTokenFromCoinSuite) TestNativeSendThenPrecompileSend() { evmtest.FunTokenGasLimitSendToEvm, ) s.Require().NoError(err) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) + s.Require().NotZero(evmResp.GasUsed) + s.Require().Greaterf(deps.Ctx.GasMeter().GasConsumed(), evmResp.GasUsed, "total gas consumed on cosmos context should be greater than gas used by EVM") s.Empty(evmResp.VmError) gasUsedFor2Ops := evmResp.GasUsed @@ -404,6 +416,7 @@ func (s *FunTokenFromCoinSuite) TestNativeSendThenPrecompileSend() { newSendAmtSendToBank, /*amount*/ ) s.Require().NoError(err) + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) evmObj, _ = deps.NewEVM() evmResp, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, @@ -415,6 +428,9 @@ func (s *FunTokenFromCoinSuite) TestNativeSendThenPrecompileSend() { evmtest.DefaultEthCallGasLimit, ) s.Require().NoError(err) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) + s.Require().NotZero(evmResp.GasUsed) + s.Require().Greaterf(deps.Ctx.GasMeter().GasConsumed(), evmResp.GasUsed, "total gas consumed on cosmos context should be greater than gas used by EVM") s.Empty(evmResp.VmError) gasUsedFor1Op := evmResp.GasUsed @@ -517,8 +533,9 @@ func (s *FunTokenFromCoinSuite) TestERC20TransferThenPrecompileSend() { big.NewInt(9e6), /*amount*/ ) s.Require().NoError(err) + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) evmObj, _ = deps.NewEVM() - _, err = deps.EvmKeeper.CallContractWithInput( + evmResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, deps.Sender.EthAddr, // from @@ -528,6 +545,9 @@ func (s *FunTokenFromCoinSuite) TestERC20TransferThenPrecompileSend() { 10_000_000, // gas limit ) s.Require().NoError(err) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) + s.Require().NotZero(evmResp.GasUsed) + s.Require().Greaterf(deps.Ctx.GasMeter().GasConsumed(), evmResp.GasUsed, "total gas consumed on cosmos context should be greater than gas used by EVM") evmtest.FunTokenBalanceAssert{ FunToken: funToken, @@ -620,6 +640,7 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { charles := evmtest.NewEthPrivAcc() s.T().Log("call test contract") + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) evmObj, _ = deps.NewEVM() contractInput, err := embeds.SmartContract_TestPrecompileSelfCallRevert.ABI.Pack( "selfCallTransferFunds", @@ -629,7 +650,7 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { big.NewInt(9e6), ) s.Require().NoError(err) - _, err = deps.EvmKeeper.CallContractWithInput( + evpResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, deps.Sender.EthAddr, @@ -639,6 +660,9 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSelfCallRevert() { evmtest.FunTokenGasLimitSendToEvm, ) s.Require().NoError(err) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) + s.Require().NotZero(evpResp.GasUsed) + s.Require().Greaterf(deps.Ctx.GasMeter().GasConsumed(), evpResp.GasUsed, "total gas consumed on cosmos context should be greater than gas used by EVM") evmtest.FunTokenBalanceAssert{ FunToken: funToken, @@ -726,8 +750,9 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSendToBankThenErc20Transfer() { "attack", ) s.Require().NoError(err) + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) evmObj, _ := deps.NewEVM() - _, err = deps.EvmKeeper.CallContractWithInput( + evpResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, deps.Sender.EthAddr, @@ -737,6 +762,9 @@ func (s *FunTokenFromCoinSuite) TestPrecompileSendToBankThenErc20Transfer() { evmtest.FunTokenGasLimitSendToEvm, ) s.Require().ErrorContains(err, "execution reverted") + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) + s.Require().NotZero(evpResp.GasUsed) + s.Require().Greaterf(deps.Ctx.GasMeter().GasConsumed(), evpResp.GasUsed, "total gas consumed on cosmos context should be greater than gas used by EVM") evmtest.FunTokenBalanceAssert{ FunToken: funToken, diff --git a/x/evm/keeper/funtoken_from_erc20_test.go b/x/evm/keeper/funtoken_from_erc20_test.go index 438b928ba..86960a2f3 100644 --- a/x/evm/keeper/funtoken_from_erc20_test.go +++ b/x/evm/keeper/funtoken_from_erc20_test.go @@ -42,7 +42,6 @@ func (s *FunTokenFromErc20Suite) TestCreateFunTokenFromERC20() { s.Require().Equal(expectedERC20Addr, deployResp.ContractAddr) evmObj, _ := deps.NewEVM() - actualMetadata, err := deps.EvmKeeper.FindERC20Metadata(deps.Ctx, evmObj, deployResp.ContractAddr, nil) s.Require().NoError(err) s.Require().Equal(metadata, *actualMetadata) @@ -75,6 +74,7 @@ func (s *FunTokenFromErc20Suite) TestCreateFunTokenFromERC20() { deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), )) + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) resp, err := deps.EvmKeeper.CreateFunToken( sdk.WrapSDKContext(deps.Ctx), &evm.MsgCreateFunToken{ @@ -83,6 +83,7 @@ func (s *FunTokenFromErc20Suite) TestCreateFunTokenFromERC20() { }, ) s.Require().NoError(err, "erc20 %s", erc20Addr) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) expectedBankDenom := fmt.Sprintf("erc20/%s", expectedERC20Addr.String()) s.Equal( @@ -199,79 +200,100 @@ func (s *FunTokenFromErc20Suite) TestSendFromEvmToBank_MadeFromErc20() { s.Require().NoError(err, "erc20 %s", deployResp.ContractAddr) bankDemon := resp.FuntokenMapping.BankDenom - s.T().Logf("mint erc20 tokens to %s", deps.Sender.EthAddr.String()) - contractInput, err := embeds.SmartContract_ERC20Minter.ABI.Pack("mint", deps.Sender.EthAddr, big.NewInt(69_420)) - s.Require().NoError(err) - evmObj, _ := deps.NewEVM() - _, err = deps.EvmKeeper.CallContractWithInput( - deps.Ctx, - evmObj, - deps.Sender.EthAddr, /*from*/ - &deployResp.ContractAddr, /*to*/ - true, /*commit*/ - contractInput, - keeper.Erc20GasLimitExecute, - ) - s.Require().NoError(err) + s.Run("happy: mint erc20 tokens", func() { + contractInput, err := embeds.SmartContract_ERC20Minter.ABI.Pack("mint", deps.Sender.EthAddr, big.NewInt(69_420)) + s.Require().NoError(err) + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + evmObj, _ := deps.NewEVM() + evmResp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, /*from*/ + &deployResp.ContractAddr, /*to*/ + true, /*commit*/ + contractInput, + keeper.Erc20GasLimitExecute, + ) + s.Require().NoError(err) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) + s.Require().NotZero(evmResp.GasUsed) + s.Require().Greater(deps.Ctx.GasMeter().GasConsumed(), evmResp.GasUsed) + }) randomAcc := testutil.AccAddress() + s.Run("happy: send erc20 tokens to Bank", func() { + contractInput, err := embeds.SmartContract_FunToken.ABI.Pack("sendToBank", deployResp.ContractAddr, big.NewInt(1), randomAcc.String()) + s.Require().NoError(err) + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + evmObj, _ := deps.NewEVM() + evmResp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, /*from*/ + &precompile.PrecompileAddr_FunToken, /*to*/ + true, /*commit*/ + contractInput, + evmtest.FunTokenGasLimitSendToEvm, + ) + s.Require().NoError(err) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) + s.Require().NotZero(evmResp.GasUsed) + s.Require().Greater(deps.Ctx.GasMeter().GasConsumed(), evmResp.GasUsed) + }) - s.T().Log("happy: send erc20 tokens to Bank") - contractInput, err = embeds.SmartContract_FunToken.ABI.Pack("sendToBank", deployResp.ContractAddr, big.NewInt(1), randomAcc.String()) - s.Require().NoError(err) - evmObj, _ = deps.NewEVM() - _, err = deps.EvmKeeper.CallContractWithInput( - deps.Ctx, - evmObj, - deps.Sender.EthAddr, /*from*/ - &precompile.PrecompileAddr_FunToken, /*to*/ - true, /*commit*/ - contractInput, - evmtest.FunTokenGasLimitSendToEvm, - ) - s.Require().NoError(err) - - s.T().Log("check balances") - evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, deps.Sender.EthAddr, big.NewInt(69_419), "expect nonzero balance") - evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(1), "expect nonzero balance") - s.Require().Equal(sdk.NewInt(1), - deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, bankDemon).Amount, - ) + s.Run("happy: check balances", func() { + evmObj, _ := deps.NewEVM() + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, deps.Sender.EthAddr, big.NewInt(69_419), "expect nonzero balance") + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(1), "expect nonzero balance") + s.Require().Equal(sdk.NewInt(1), + deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, bankDemon).Amount, + ) + }) - s.T().Log("sad: send too many erc20 tokens to Bank") - contractInput, err = embeds.SmartContract_FunToken.ABI.Pack("sendToBank", deployResp.ContractAddr, big.NewInt(70_000), randomAcc.String()) - s.Require().NoError(err) - evmObj, _ = deps.NewEVM() - evmResp, err := deps.EvmKeeper.CallContractWithInput( - deps.Ctx, - evmObj, - deps.Sender.EthAddr, /*from*/ - &precompile.PrecompileAddr_FunToken, /*to*/ - true, /*commit*/ - contractInput, - evmtest.FunTokenGasLimitSendToEvm, - ) - s.Require().Error(err, evmResp.String()) + s.Run("sad: send too many erc20 tokens to Bank", func() { + contractInput, err := embeds.SmartContract_FunToken.ABI.Pack("sendToBank", deployResp.ContractAddr, big.NewInt(70_000), randomAcc.String()) + s.Require().NoError(err) + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + evmObj, _ := deps.NewEVM() + evmResp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, + evmObj, + deps.Sender.EthAddr, /*from*/ + &precompile.PrecompileAddr_FunToken, /*to*/ + true, /*commit*/ + contractInput, + evmtest.FunTokenGasLimitSendToEvm, + ) + s.Require().Error(err, evmResp.String()) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) + s.Require().NotZero(evmResp.GasUsed) + s.Require().Greater(deps.Ctx.GasMeter().GasConsumed(), evmResp.GasUsed) + }) - s.T().Log("send Bank tokens back to erc20") - _, err = deps.EvmKeeper.ConvertCoinToEvm(sdk.WrapSDKContext(deps.Ctx), - &evm.MsgConvertCoinToEvm{ - ToEthAddr: eth.EIP55Addr{ - Address: deps.Sender.EthAddr, + s.Run("happy: send Bank tokens back to erc20", func() { + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + _, err = deps.EvmKeeper.ConvertCoinToEvm(sdk.WrapSDKContext(deps.Ctx), + &evm.MsgConvertCoinToEvm{ + ToEthAddr: eth.EIP55Addr{ + Address: deps.Sender.EthAddr, + }, + Sender: randomAcc.String(), + BankCoin: sdk.NewCoin(bankDemon, sdk.NewInt(1)), }, - Sender: randomAcc.String(), - BankCoin: sdk.NewCoin(bankDemon, sdk.NewInt(1)), - }, - ) - s.Require().NoError(err) + ) + s.Require().NoError(err) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) + }) s.T().Log("check balances") - evmObj, _ = deps.NewEVM() - evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, deps.Sender.EthAddr, big.NewInt(69_420), "expect nonzero balance") - evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(0), "expect nonzero balance") - s.Require().True( - deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, bankDemon).Amount.Equal(sdk.NewInt(0)), - ) + s.Run("happy: check balances", func() { + evmObj, _ := deps.NewEVM() + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, deps.Sender.EthAddr, big.NewInt(69_420), "expect nonzero balance") + evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(0), "expect nonzero balance") + s.Require().True( + deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, bankDemon).Amount.Equal(sdk.NewInt(0)), + ) + }) s.T().Log("sad: send too many Bank tokens back to erc20") _, err = deps.EvmKeeper.ConvertCoinToEvm(sdk.WrapSDKContext(deps.Ctx), @@ -368,8 +390,9 @@ func (s *FunTokenFromErc20Suite) TestFunTokenFromERC20MaliciousTransfer() { s.T().Log("send erc20 tokens to cosmos") input, err := embeds.SmartContract_FunToken.ABI.Pack("sendToBank", deployResp.ContractAddr, big.NewInt(1), randomAcc.String()) s.Require().NoError(err) + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) evmObj, _ := deps.NewEVM() - _, err = deps.EvmKeeper.CallContractWithInput( + evmResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, evm.EVM_MODULE_ADDRESS, @@ -379,6 +402,9 @@ func (s *FunTokenFromErc20Suite) TestFunTokenFromERC20MaliciousTransfer() { evmtest.FunTokenGasLimitSendToEvm, ) s.Require().ErrorContains(err, "gas required exceeds allowance") + s.Require().NotZero(evmResp.GasUsed) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) + s.Require().Greater(deps.Ctx.GasMeter().GasConsumed(), evmResp.GasUsed) } // TestFunTokenInfiniteRecursionERC20 creates a funtoken from a contract @@ -422,7 +448,7 @@ func (s *FunTokenFromErc20Suite) TestFunTokenInfiniteRecursionERC20() { contractInput, err := embeds.SmartContract_TestInfiniteRecursionERC20.ABI.Pack("attackBalance") s.Require().NoError(err) evmObj, _ := deps.NewEVM() - _, err = deps.EvmKeeper.CallContractWithInput( + evmResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, deps.Sender.EthAddr, /*from*/ @@ -432,12 +458,15 @@ func (s *FunTokenFromErc20Suite) TestFunTokenInfiniteRecursionERC20() { 10_000_000, ) s.Require().NoError(err) + s.Require().NotZero(evmResp.GasUsed) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) + s.Require().Greater(deps.Ctx.GasMeter().GasConsumed(), evmResp.GasUsed) s.T().Log("sad: call attackTransfer()") contractInput, err = embeds.SmartContract_TestInfiniteRecursionERC20.ABI.Pack("attackTransfer") s.Require().NoError(err) evmObj, _ = deps.NewEVM() - _, err = deps.EvmKeeper.CallContractWithInput( + evmResp, err = deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, deps.Sender.EthAddr, /*from*/ @@ -447,6 +476,9 @@ func (s *FunTokenFromErc20Suite) TestFunTokenInfiniteRecursionERC20() { 10_000_000, ) s.Require().ErrorContains(err, "execution reverted") + s.Require().NotZero(evmResp.GasUsed) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) + s.Require().Greater(deps.Ctx.GasMeter().GasConsumed(), evmResp.GasUsed) } // TestSendERC20WithFee creates a funtoken from a malicious contract which charges a 10% fee on any transfer. @@ -494,8 +526,9 @@ func (s *FunTokenFromErc20Suite) TestSendERC20WithFee() { randomAcc.String(), /*to*/ ) s.Require().NoError(err) + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) evmObj, _ := deps.NewEVM() - _, err = deps.EvmKeeper.CallContractWithInput( + evmResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, deps.Sender.EthAddr, /*from*/ @@ -505,6 +538,9 @@ func (s *FunTokenFromErc20Suite) TestSendERC20WithFee() { evmtest.FunTokenGasLimitSendToEvm, ) s.Require().NoError(err) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) + s.Require().NotZero(evmResp.GasUsed) + s.Require().Greater(deps.Ctx.GasMeter().GasConsumed(), evmResp.GasUsed) s.T().Log("check balances") evmtest.AssertERC20BalanceEqualWithDescription(s.T(), deps, evmObj, deployResp.ContractAddr, deps.Sender.EthAddr, big.NewInt(900), "expect 900 balance") @@ -569,8 +605,9 @@ func (s *FunTokenFromErc20Suite) TestFindMKRMetadata() { ) s.Require().NoError(err) + deps.Ctx = deps.Ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) evmObj, _ := deps.NewEVM() - _, err = deps.EvmKeeper.CallContractWithInput( + evmResp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, deps.Sender.EthAddr, @@ -579,8 +616,10 @@ func (s *FunTokenFromErc20Suite) TestFindMKRMetadata() { contractInput, evmtest.FunTokenGasLimitSendToEvm, ) - s.Require().NoError(err) + s.Require().NotZero(deps.Ctx.GasMeter().GasConsumed()) + s.Require().NotZero(evmResp.GasUsed) + s.Require().Greater(deps.Ctx.GasMeter().GasConsumed(), evmResp.GasUsed) info, err := deps.EvmKeeper.FindERC20Metadata(deps.Ctx, evmObj, deployResp.ContractAddr, embeds.SmartContract_TestBytes32Metadata.ABI) s.Require().NoError(err) diff --git a/x/evm/keeper/gas_fees.go b/x/evm/keeper/gas_fees.go index d5712c067..8b8a6a495 100644 --- a/x/evm/keeper/gas_fees.go +++ b/x/evm/keeper/gas_fees.go @@ -74,16 +74,16 @@ func (k *Keeper) RefundGas( return nil } -// GasToRefund calculates the amount of gas the state machine should refund to -// the sender. It is capped by the refund quotient value. Note that passing a -// jrefundQuotient of 0 will cause problems. -func GasToRefund(availableRefund, gasConsumed, refundQuotient uint64) uint64 { - // Apply refund counter - refund := gasConsumed / refundQuotient - if refund > availableRefund { - return availableRefund +// gasToRefund calculates the amount of gas the state machine should refund to +// the sender. +// EIP-3529: refunds are capped to gasUsed / 5 +func gasToRefund(availableRefundAmount, gasUsed uint64) uint64 { + refundAmount := gasUsed / params.RefundQuotientEIP3529 + if refundAmount > availableRefundAmount { + // Apply refundAmount counter + return availableRefundAmount } - return refund + return refundAmount } // CheckSenderBalance validates that the tx cost value is positive and that the diff --git a/x/evm/keeper/grpc_query.go b/x/evm/keeper/grpc_query.go index 3255b8f73..da2274d50 100644 --- a/x/evm/keeper/grpc_query.go +++ b/x/evm/keeper/grpc_query.go @@ -286,7 +286,7 @@ func (k *Keeper) EthCall( // pass false to not commit StateDB stateDB := statedb.New(ctx, k, txConfig) evm := k.NewEVM(ctx, msg, evmCfg, nil /*tracer*/, stateDB) - res, err := k.ApplyEvmMsg(ctx, msg, evm, nil /*tracer*/, false /*commit*/, txConfig.TxHash, false /*fullRefundLeftoverGas*/) + res, err := k.ApplyEvmMsg(ctx, msg, evm, nil /*tracer*/, false /*commit*/, txConfig.TxHash) if err != nil { return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) } @@ -421,7 +421,7 @@ func (k Keeper) EstimateGasForEvmCallType( txConfig := statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes())) stateDB := statedb.New(ctx, &k, txConfig) evmObj := k.NewEVM(tmpCtx, evmMsg, evmCfg, nil /*tracer*/, stateDB) - rsp, err = k.ApplyEvmMsg(tmpCtx, evmMsg, evmObj, nil /*tracer*/, false /*commit*/, txConfig.TxHash, false /*fullRefundLeftoverGas*/) + rsp, err = k.ApplyEvmMsg(tmpCtx, evmMsg, evmObj, nil /*tracer*/, false /*commit*/, txConfig.TxHash) if err != nil { if errors.Is(err, core.ErrIntrinsicGas) { return true, nil, nil // Special case, raise gas limit @@ -516,7 +516,7 @@ func (k Keeper) TraceTx( WithTransientKVGasConfig(storetypes.GasConfig{}) stateDB := statedb.New(ctx, &k, txConfig) evmObj := k.NewEVM(ctx, msg, evmCfg, nil /*tracer*/, stateDB) - rsp, err := k.ApplyEvmMsg(ctx, msg, evmObj, nil /*tracer*/, false /*commit*/, txConfig.TxHash, false /*fullRefundLeftoverGas*/) + rsp, err := k.ApplyEvmMsg(ctx, msg, evmObj, nil /*tracer*/, false /*commit*/, txConfig.TxHash) if err != nil { continue } @@ -790,7 +790,7 @@ func (k *Keeper) TraceEthTxMsg( WithTransientKVGasConfig(storetypes.GasConfig{}) stateDB := statedb.New(ctx, k, txConfig) evmObj := k.NewEVM(ctx, msg, evmCfg, tracer, stateDB) - res, err := k.ApplyEvmMsg(ctx, msg, evmObj, tracer, false /*commit*/, txConfig.TxHash, false /*fullRefundLeftoverGas*/) + res, err := k.ApplyEvmMsg(ctx, msg, evmObj, tracer, false /*commit*/, txConfig.TxHash) 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 ff36cd6cb..19de7266d 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -128,13 +128,12 @@ func HandleOutOfGasPanic(err *error, format string) func() { if r := recover(); r != nil { switch r.(type) { case sdk.ErrorOutOfGas: - *err = vm.ErrOutOfGas default: panic(r) } } - if err != nil && format != "" { + if err != nil && *err != nil && format != "" { *err = fmt.Errorf("%s: %w", format, *err) } } diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 7c6e98021..e5421725c 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -10,7 +10,6 @@ import ( "strconv" "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" @@ -71,8 +70,10 @@ func (k *Keeper) EthereumTx( nil, /*tracer*/ true, /*commit*/ txConfig.TxHash, - false, /*fullRefundLeftoverGas*/ ) + if evmResp != nil { + ctx.GasMeter().ConsumeGas(evmResp.GasUsed, "execute ethereum tx") + } if err != nil { return nil, errors.Wrap(err, "error applying ethereum core message") } @@ -89,7 +90,6 @@ func (k *Keeper) EthereumTx( if err = k.RefundGas(ctx, evmMsg.From(), refundGas, weiPerGas); err != nil { return nil, errors.Wrapf(err, "error refunding leftover gas to sender %s", evmMsg.From()) } - ctx.GasMeter().ConsumeGas(evmResp.GasUsed, "execute ethereum tx") err = k.EmitEthereumTxEvents(ctx, tx.To(), tx.Type(), evmMsg, evmResp) if err != nil { @@ -259,23 +259,21 @@ func (k *Keeper) ApplyEvmMsg( tracer vm.EVMLogger, commit bool, txHash gethcommon.Hash, - fullRefundLeftoverGas bool, ) (resp *evm.MsgEthereumTxResponse, err error) { - leftoverGas := msg.Gas() + gasRemaining := msg.Gas() // Allow the tracer captures the tx level events, mainly the gas consumption. vmCfg := evmObj.Config if vmCfg.Debug { - vmCfg.Tracer.CaptureTxStart(leftoverGas) + vmCfg.Tracer.CaptureTxStart(gasRemaining) defer func() { - vmCfg.Tracer.CaptureTxEnd(leftoverGas) + vmCfg.Tracer.CaptureTxEnd(gasRemaining) }() } - sender := vm.AccountRef(msg.From()) contractCreation := msg.To() == nil - intrinsicGas, err := core.IntrinsicGas( + intrinsicGasCost, err := core.IntrinsicGas( msg.Data(), msg.AccessList(), contractCreation, true, true, ) @@ -290,15 +288,15 @@ func (k *Keeper) ApplyEvmMsg( // // Should check again even if it is checked on Ante Handler, because eth_call // don't go through Ante Handler. - if leftoverGas < intrinsicGas { + if gasRemaining < intrinsicGasCost { // eth_estimateGas will check for this exact error return nil, errors.Wrapf( core.ErrIntrinsicGas, "ApplyEvmMsg: provided msg.Gas (%d) is less than intrinsic gas cost (%d)", - leftoverGas, intrinsicGas, + gasRemaining, intrinsicGasCost, ) } - leftoverGas = leftoverGas - intrinsicGas + gasRemaining -= intrinsicGasCost // access list preparation is moved from ante handler to here, because it's // needed when `ApplyMessage` is called under contexts where ante handlers @@ -318,28 +316,28 @@ func (k *Keeper) ApplyEvmMsg( // take over the nonce management from evm: // - reset sender's nonce to msg.Nonce() before calling evm. // - increase sender's nonce by one no matter the result. - evmObj.StateDB.SetNonce(sender.Address(), msg.Nonce()) + evmObj.StateDB.SetNonce(msg.From(), msg.Nonce()) - var ret []byte + var returnBz []byte var vmErr error if contractCreation { - ret, _, leftoverGas, vmErr = evmObj.Create( - sender, + returnBz, _, gasRemaining, vmErr = evmObj.Create( + vm.AccountRef(msg.From()), msg.Data(), - leftoverGas, + gasRemaining, msgWei, ) } else { - ret, leftoverGas, vmErr = evmObj.Call( - sender, + returnBz, gasRemaining, vmErr = evmObj.Call( + vm.AccountRef(msg.From()), *msg.To(), msg.Data(), - leftoverGas, + gasRemaining, msgWei, ) } // Increment nonce after processing the message - evmObj.StateDB.SetNonce(sender.Address(), msg.Nonce()+1) + evmObj.StateDB.SetNonce(msg.From(), msg.Nonce()+1) // EVM execution error needs to be available for the JSON-RPC client var vmError string @@ -347,57 +345,34 @@ func (k *Keeper) ApplyEvmMsg( vmError = vmErr.Error() } - // The dirty states in `StateDB` is either committed or discarded after return - if commit { - if err := evmObj.StateDB.(*statedb.StateDB).Commit(); err != nil { - return nil, errors.Wrap(err, "ApplyEvmMsg: failed to commit stateDB") - } - } - // Rare case of uint64 gas overflow - if msg.Gas() < leftoverGas { - return nil, errors.Wrapf(core.ErrGasUintOverflow, "ApplyEvmMsg: message gas limit (%d) < leftover gas (%d)", msg.Gas(), leftoverGas) - } + // process gas refunds (we refund a portion of the unused gas) + gasUsed := msg.Gas() - gasRemaining + // please see https://eips.ethereum.org/EIPS/eip-3529 for why we do refunds + refundAmount := gasToRefund(evmObj.StateDB.GetRefund(), gasUsed) + gasRemaining += refundAmount + gasUsed -= refundAmount - // TODO: UD-DEBUG: Clarify text below. - // 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% - // - // EIP-3529: refunds are capped to gasUsed / 5 - // We evaluate "fullRefundLeftoverGas" and use only the gas consumed (not the - // gas limit) if the `ApplyEvmMsg` call originated from a state transition - // where the chain set the gas limit and not an end-user. - refundQuotient := params.RefundQuotientEIP3529 - if fullRefundLeftoverGas { - refundQuotient = 1 // 100% refund + evmResp := &evm.MsgEthereumTxResponse{ + GasUsed: gasUsed, + VmError: vmError, + Ret: returnBz, + Logs: evm.NewLogsFromEth(evmObj.StateDB.(*statedb.StateDB).Logs()), + Hash: txHash.Hex(), } - temporaryGasUsed := msg.Gas() - leftoverGas - refund := GasToRefund(evmObj.StateDB.GetRefund(), temporaryGasUsed, refundQuotient) - // update leftoverGas and temporaryGasUsed with refund amount - leftoverGas += refund - temporaryGasUsed -= refund - if msg.Gas() < leftoverGas { - return nil, errors.Wrapf(core.ErrGasUintOverflow, "ApplyEvmMsg: message gas limit (%d) < leftover gas (%d)", msg.Gas(), leftoverGas) + if gasRemaining > msg.Gas() { // rare case of overflow + evmResp.GasUsed = msg.Gas() // cap the gas used to the original gas limit + return evmResp, errors.Wrapf(core.ErrGasUintOverflow, "ApplyEvmMsg: message gas limit (%d) < leftover gas (%d)", msg.Gas(), gasRemaining) } - // Min gas used is a % of gasLimit - gasUsed := math.LegacyNewDec(int64(temporaryGasUsed)).TruncateInt().Uint64() - - // This resulting "leftoverGas" is used by the tracer. This happens as a - // result of the defer statement near the beginning of the function with - // "vm.Tracer". - leftoverGas = msg.Gas() - gasUsed + // The dirty states in `StateDB` is either committed or discarded after return + if commit { + if err := evmObj.StateDB.(*statedb.StateDB).Commit(); err != nil { + return evmResp, errors.Wrap(err, "ApplyEvmMsg: failed to commit stateDB") + } + } - return &evm.MsgEthereumTxResponse{ - GasUsed: gasUsed, - VmError: vmError, - Ret: ret, - Logs: evm.NewLogsFromEth(evmObj.StateDB.(*statedb.StateDB).Logs()), - Hash: txHash.Hex(), - }, nil + return evmResp, nil } func ParseWeiAsMultipleOfMicronibi(weiInt *big.Int) (newWeiInt *big.Int, err error) { @@ -560,6 +535,7 @@ func (k Keeper) convertCoinToEvmBornCoin( defer func() { k.Bank.StateDB = nil }() + evmObj := k.NewEVM(ctx, evmMsg, k.GetEVMConfig(ctx), nil /*tracer*/, stateDB) evmResp, err := k.CallContractWithInput( ctx, @@ -573,15 +549,13 @@ func (k Keeper) convertCoinToEvmBornCoin( if err != nil { return nil, err } - ctx.GasMeter().ConsumeGas(evmResp.GasUsed, "mint erc20 tokens") if evmResp.Failed() { return nil, fmt.Errorf("failed to mint erc-20 tokens of contract %s", erc20Addr.String()) } - err = stateDB.Commit() - if err != nil { + if err = stateDB.Commit(); err != nil { return nil, errors.Wrap(err, "failed to commit stateDB") } diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go index 6a1aa210b..82739c612 100644 --- a/x/evm/precompile/funtoken.go +++ b/x/evm/precompile/funtoken.go @@ -143,8 +143,7 @@ func (p precompileFunToken) sendToBank( erc20, amount, to, err := p.parseArgsSendToBank(args) if err != nil { - err = ErrInvalidArgs(err) - return + return nil, ErrInvalidArgs(err) } var evmResponses []*evm.MsgEthereumTxResponse @@ -191,10 +190,9 @@ func (p precompileFunToken) sendToBank( // owns the ERC20 contract and was the original minter of the ERC20 tokens. // Since we're sending them away and want accurate total supply tracking, the // tokens need to be burned. - burnResp, e := p.evmKeeper.ERC20().Burn(erc20, evm.EVM_MODULE_ADDRESS, gotAmount, ctx, evmObj) - if e != nil { - err = fmt.Errorf("ERC20.Burn: %w", e) - return + burnResp, err := p.evmKeeper.ERC20().Burn(erc20, evm.EVM_MODULE_ADDRESS, gotAmount, ctx, evmObj) + if err != nil { + return nil, fmt.Errorf("ERC20.Burn: %w", err) } evmResponses = append(evmResponses, burnResp) } else { diff --git a/x/evm/precompile/funtoken_test.go b/x/evm/precompile/funtoken_test.go index a5b21f60d..be9a33714 100644 --- a/x/evm/precompile/funtoken_test.go +++ b/x/evm/precompile/funtoken_test.go @@ -267,13 +267,14 @@ func (s *FuntokenSuite) TestPrecompileLocalGas() { s.Require().NoError(err) contractAddr := deployResp.ContractAddr - s.T().Log("Fund sender's wallet") - s.Require().NoError(testapp.FundAccount( - deps.App.BankKeeper, - deps.Ctx, - deps.Sender.NibiruAddr, - sdk.NewCoins(sdk.NewCoin(funtoken.BankDenom, sdk.NewInt(1000))), - )) + s.Run("Fund sender's wallet", func() { + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + sdk.NewCoins(sdk.NewCoin(funtoken.BankDenom, sdk.NewInt(1000))), + )) + }) s.Run("Fund contract with erc20 coins", func() { _, err = deps.EvmKeeper.ConvertCoinToEvm( @@ -297,7 +298,7 @@ func (s *FuntokenSuite) TestPrecompileLocalGas() { ) s.Require().NoError(err) evmObj, _ := deps.NewEVM() - _, err = deps.EvmKeeper.CallContractWithInput( + resp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, deps.Sender.EthAddr, @@ -307,6 +308,7 @@ func (s *FuntokenSuite) TestPrecompileLocalGas() { evmtest.FunTokenGasLimitSendToEvm, ) s.Require().NoError(err) + s.Require().NotZero(resp.GasUsed) }) s.Run("Happy: callBankSend with local gas - sufficient gas amount", func() { @@ -318,7 +320,7 @@ func (s *FuntokenSuite) TestPrecompileLocalGas() { ) s.Require().NoError(err) evmObj, _ := deps.NewEVM() - _, err = deps.EvmKeeper.CallContractWithInput( + resp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, deps.Sender.EthAddr, @@ -328,6 +330,7 @@ func (s *FuntokenSuite) TestPrecompileLocalGas() { evmtest.FunTokenGasLimitSendToEvm, // gasLimit for the entire call ) s.Require().NoError(err) + s.Require().NotZero(resp.GasUsed) }) s.Run("Sad: callBankSend with local gas - insufficient gas amount", func() { @@ -339,7 +342,7 @@ func (s *FuntokenSuite) TestPrecompileLocalGas() { ) s.Require().NoError(err) evmObj, _ := deps.NewEVM() - _, err = deps.EvmKeeper.CallContractWithInput( + resp, err := deps.EvmKeeper.CallContractWithInput( deps.Ctx, evmObj, deps.Sender.EthAddr, @@ -349,6 +352,7 @@ func (s *FuntokenSuite) TestPrecompileLocalGas() { evmtest.FunTokenGasLimitSendToEvm, // gasLimit for the entire call ) s.Require().ErrorContains(err, "execution reverted") + s.Require().NotZero(resp.GasUsed) }) }