From 8288315a11b74bae0763530a8bb30a6c935d3e83 Mon Sep 17 00:00:00 2001 From: Jhelison Uchoa Date: Wed, 16 Jul 2025 18:28:11 -0300 Subject: [PATCH 01/12] feat: update the gas refundGas function to handle paied fees from the context --- x/vm/keeper/gas.go | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/x/vm/keeper/gas.go b/x/vm/keeper/gas.go index b8dc6bc5..c14ee65a 100644 --- a/x/vm/keeper/gas.go +++ b/x/vm/keeper/gas.go @@ -15,6 +15,9 @@ import ( "github.com/cosmos/evm/x/vm/types" ) +// ContextPaidFeesKey is a key used to store the paid fee in the context +type ContextPaidFeesKey struct{} + // GetEthIntrinsicGas returns the intrinsic gas cost for the transaction func (k *Keeper) GetEthIntrinsicGas(ctx sdk.Context, msg core.Message, cfg *params.ChainConfig, isContractCreation bool) (uint64, error) { height := big.NewInt(ctx.BlockHeight()) @@ -37,17 +40,44 @@ func (k *Keeper) RefundGas(ctx sdk.Context, msg core.Message, leftoverGas uint64 // negative refund errors return errorsmod.Wrapf(types.ErrInvalidRefund, "refunded amount value cannot be negative %d", remaining.Int64()) case 1: - // positive amount refund + // Attempt to extract the paid coin from the context + // This is used when fee abstraction is applied into the fee payment + // If no value is found under the context, the original denom is used + if val := ctx.Value(ContextPaidFeesKey{}); val != nil { + // We check if a coin exists under the value and if its not empty + if paidCoins, ok := val.(sdk.Coins); ok && !paidCoins.IsZero() { + // We know that only a single coin is used for EVM payments + if len(paidCoins) != 1 { + // This should never happen, but if it does, we return an error + return errorsmod.Wrapf(types.ErrInvalidRefund, "expected a single coin for EVM refunds, got %d", len(paidCoins)) + } + paidCoin := paidCoins[0] + + // Extract the coin information + denom = paidCoin.Denom + amount := paidCoin.Amount.BigInt() + + // Calculate the amount to refund + // This is calculated as: + // remaining = amount * leftoverGas / gasUsed + remaining = new(big.Int).Div( + new(big.Int).Mul(amount, new(big.Int).SetUint64(leftoverGas)), + new(big.Int).SetUint64(msg.Gas()), + ) + } + } + + // Positive amount refund refundedCoins := sdk.Coins{sdk.NewCoin(denom, sdkmath.NewIntFromBigInt(remaining))} - // refund to sender from the fee collector module account, which is the escrow account in charge of collecting tx fees + // Refund to sender from the fee collector module account, which is the escrow account in charge of collecting tx fees err := k.bankWrapper.SendCoinsFromModuleToAccount(ctx, authtypes.FeeCollectorName, msg.From().Bytes(), refundedCoins) if err != nil { err = errorsmod.Wrapf(errortypes.ErrInsufficientFunds, "fee collector account failed to refund fees: %s", err.Error()) return errorsmod.Wrapf(err, "failed to refund %d leftover gas (%s)", leftoverGas, refundedCoins.String()) } default: - // no refund, consume gas and update the tx gas meter + // No refund, consume gas and update the tx gas meter } return nil From e5f134ffbfe1fa766275d5b07db26a35dd39bd1d Mon Sep 17 00:00:00 2001 From: Jhelison Uchoa Date: Wed, 16 Jul 2025 18:28:45 -0300 Subject: [PATCH 02/12] test: add tests to the new logic for gasRefunds --- x/vm/keeper/gas_test.go | 205 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 x/vm/keeper/gas_test.go diff --git a/x/vm/keeper/gas_test.go b/x/vm/keeper/gas_test.go new file mode 100644 index 00000000..30f7724f --- /dev/null +++ b/x/vm/keeper/gas_test.go @@ -0,0 +1,205 @@ +package keeper_test + +import ( + "math/big" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/evm/testutil/integration/os/factory" + "github.com/cosmos/evm/testutil/integration/os/grpc" + testkeyring "github.com/cosmos/evm/testutil/integration/os/keyring" + erc20mocks "github.com/cosmos/evm/x/erc20/types/mocks" + "github.com/cosmos/evm/x/vm/keeper" + "github.com/cosmos/evm/x/vm/types" + "go.uber.org/mock/gomock" +) + +const ( + DefaultCoreMsgGasUsage = 21000 + DefaultGasPrice = 120000 +) + +// TestGasRefundGas tests the refund gas exclusively without going though the state transition +// The gas part on the name refers to the file name to not generate a duplicated test name +func (suite *KeeperTestSuite) TestGasRefundGas() { + // Create a txFactory + grpcHandler := grpc.NewIntegrationHandler(suite.network) + txFactory := factory.New(suite.network, grpcHandler) + + // Create a core message to use for the test + keyring := testkeyring.New(2) + sender := keyring.GetKey(0) + recipient := keyring.GetAddr(1) + coreMsg, err := txFactory.GenerateGethCoreMsg( + sender.Priv, + types.EvmTxArgs{ + To: &recipient, + Amount: big.NewInt(100), + GasPrice: big.NewInt(120000), + }, + ) + suite.Require().NoError(err) + + // Produce all the test cases + testCases := []struct { + name string + leftoverGas uint64 // The coreMsg always uses 21000 gas limit + malleate func(sdk.Context) sdk.Context + expectedRefund sdk.Coins + errContains string + }{ + { + name: "Refund the full value as no gas was used", + leftoverGas: DefaultCoreMsgGasUsage, + expectedRefund: sdk.NewCoins( + sdk.NewCoin(suite.network.GetBaseDenom(), sdkmath.NewInt(DefaultCoreMsgGasUsage*DefaultGasPrice)), + ), + }, + { + name: "Refund half the value as half gas was used", + leftoverGas: DefaultCoreMsgGasUsage / 2, + expectedRefund: sdk.NewCoins( + sdk.NewCoin(suite.network.GetBaseDenom(), sdkmath.NewInt((DefaultCoreMsgGasUsage*DefaultGasPrice)/2)), + ), + }, + { + name: "No refund as no gas was left over used", + leftoverGas: 0, + expectedRefund: sdk.NewCoins( + sdk.NewCoin(suite.network.GetBaseDenom(), sdkmath.NewInt(0)), + ), + }, + { + name: "Refund with context fees, refunding the full value", + leftoverGas: DefaultCoreMsgGasUsage, + malleate: func(ctx sdk.Context) sdk.Context { + // Set the fee abstraction paid fee key with a single coin + return ctx.WithValue( + keeper.ContextPaidFeesKey{}, + sdk.NewCoins( + sdk.NewCoin("acoin", sdkmath.NewInt(750_000_000)), + ), + ) + }, + expectedRefund: sdk.NewCoins( + sdk.NewCoin("acoin", sdkmath.NewInt(750_000_000)), + ), + }, + { + name: "Refund with context fees, refunding the half the value", + leftoverGas: DefaultCoreMsgGasUsage / 2, + malleate: func(ctx sdk.Context) sdk.Context { + // Set the fee abstraction paid fee key with a single coin + return ctx.WithValue( + keeper.ContextPaidFeesKey{}, + sdk.NewCoins( + sdk.NewCoin("acoin", sdkmath.NewInt(750_000_000)), + ), + ) + }, + expectedRefund: sdk.NewCoins( + sdk.NewCoin("acoin", sdkmath.NewInt(750_000_000/2)), + ), + }, + { + name: "Refund with context fees, no refund", + leftoverGas: 0, + malleate: func(ctx sdk.Context) sdk.Context { + // Set the fee abstraction paid fee key with a single coin + return ctx.WithValue( + keeper.ContextPaidFeesKey{}, + sdk.NewCoins( + sdk.NewCoin("acoin", sdkmath.NewInt(750_000_000)), + ), + ) + }, + expectedRefund: sdk.NewCoins( + sdk.NewCoin("acoin", sdkmath.NewInt(0)), + ), + }, + { + name: "Error - More than one coin being passed", + leftoverGas: DefaultCoreMsgGasUsage, + malleate: func(ctx sdk.Context) sdk.Context { + // Set the fee abstraction paid fee key with a single coin + return ctx.WithValue( + keeper.ContextPaidFeesKey{}, + sdk.NewCoins( + sdk.NewCoin("acoin", sdkmath.NewInt(750_000_000)), + sdk.NewCoin("atwo", sdkmath.NewInt(750_000_000)), + ), + ) + }, + expectedRefund: sdk.NewCoins( + sdk.NewCoin("acoin", sdkmath.NewInt(0)), // We say as zero to skip the mock bank check + ), + errContains: "expected a single coin for EVM refunds, got 2", + }, + } + + // Iterate though the test cases + for _, tc := range testCases { + suite.Run(tc.name, func() { + // Generate a cached context to not leak data between tests + ctx, _ := suite.network.GetContext().CacheContext() + + // Create a new controller for the mock + ctrl := gomock.NewController(suite.T()) + defer ctrl.Finish() + + // Apply the malleate function to the context + if tc.malleate != nil { + ctx = tc.malleate(ctx) + } + + // Create a new mock bank keeper + mockBankKeeper := erc20mocks.NewMockBankKeeper(ctrl) + + // Apply the expect, but only if expected refund is not zero + if !tc.expectedRefund.IsZero() { + mockBankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx sdk.Context, senderModule string, recipient sdk.AccAddress, coins sdk.Coins) error { + if !coins.Equal(tc.expectedRefund) { + suite.T().Errorf("expected %s, got %s", tc.expectedRefund, coins) + } + + return nil + }) + } + + // Initialize a new EVM keeper with the mock bank keeper + // We need to redo this every time, since we will apply the mocked bank keeper at this step + evmKeeper := keeper.NewKeeper( + suite.network.App.AppCodec(), + suite.network.App.GetKey(types.StoreKey), + suite.network.App.GetTKey(types.StoreKey), + authtypes.NewModuleAddress(govtypes.ModuleName), + suite.network.App.AccountKeeper, + mockBankKeeper, + suite.network.App.StakingKeeper, + suite.network.App.FeeMarketKeeper, + suite.network.App.Erc20Keeper, + "", + suite.network.App.GetSubspace(types.ModuleName), + ) + + // Call the msg, not further checks are needed, all balance checks are done in the mock + err := evmKeeper.RefundGas( + ctx, + coreMsg, + tc.leftoverGas, + suite.network.GetBaseDenom(), + ) + + // Check the error + if tc.errContains != "" { + suite.Require().ErrorContains(err, tc.errContains, "RefundGas should return an error") + } else { + suite.Require().NoError(err, "RefundGas should not return an error") + } + }) + } + +} From 102da88df601e5beecffcf7a31d896bf959f2e10 Mon Sep 17 00:00:00 2001 From: Jhelison Uchoa <68653689+jhelison@users.noreply.github.com> Date: Wed, 16 Jul 2025 18:36:13 -0300 Subject: [PATCH 03/12] refactor: apply typo fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- x/vm/keeper/gas.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/vm/keeper/gas.go b/x/vm/keeper/gas.go index c14ee65a..57ec8e12 100644 --- a/x/vm/keeper/gas.go +++ b/x/vm/keeper/gas.go @@ -44,7 +44,7 @@ func (k *Keeper) RefundGas(ctx sdk.Context, msg core.Message, leftoverGas uint64 // This is used when fee abstraction is applied into the fee payment // If no value is found under the context, the original denom is used if val := ctx.Value(ContextPaidFeesKey{}); val != nil { - // We check if a coin exists under the value and if its not empty + // We check if a coin exists under the value and if it's not empty if paidCoins, ok := val.(sdk.Coins); ok && !paidCoins.IsZero() { // We know that only a single coin is used for EVM payments if len(paidCoins) != 1 { From bdff4ca94c2c477809ffaec69812f48dbc95d108 Mon Sep 17 00:00:00 2001 From: Jhelison Uchoa Date: Wed, 16 Jul 2025 18:37:43 -0300 Subject: [PATCH 04/12] feat: add check if gas is zero --- x/vm/keeper/gas.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x/vm/keeper/gas.go b/x/vm/keeper/gas.go index 57ec8e12..f7de7f02 100644 --- a/x/vm/keeper/gas.go +++ b/x/vm/keeper/gas.go @@ -35,6 +35,12 @@ func (k *Keeper) RefundGas(ctx sdk.Context, msg core.Message, leftoverGas uint64 // Return EVM tokens for remaining gas, exchanged at the original rate. remaining := new(big.Int).Mul(new(big.Int).SetUint64(leftoverGas), msg.GasPrice()) + // Check if gas is zero + if msg.Gas() == 0 { + // If gas is zero, we cannot refund anything, so we return early + return nil + } + switch remaining.Sign() { case -1: // negative refund errors From 42a68322465a57f642e513ac91cf84596e693f6d Mon Sep 17 00:00:00 2001 From: Thales Zirbel Date: Thu, 18 Dec 2025 12:55:45 -0300 Subject: [PATCH 05/12] feat: fix unconsistent states --- .../integration/x/vm/test_gas.go | 71 ++++++------------- .../integration/x/vm/test_state_transition.go | 1 + x/vm/keeper/gas.go | 6 +- x/vm/keeper/state_transition.go | 2 +- 4 files changed, 26 insertions(+), 54 deletions(-) rename x/vm/keeper/gas_test.go => tests/integration/x/vm/test_gas.go (65%) diff --git a/x/vm/keeper/gas_test.go b/tests/integration/x/vm/test_gas.go similarity index 65% rename from x/vm/keeper/gas_test.go rename to tests/integration/x/vm/test_gas.go index 30f7724f..dbe2e7e3 100644 --- a/x/vm/keeper/gas_test.go +++ b/tests/integration/x/vm/test_gas.go @@ -1,19 +1,16 @@ -package keeper_test +package vm import ( "math/big" sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" - "github.com/cosmos/evm/testutil/integration/os/factory" - "github.com/cosmos/evm/testutil/integration/os/grpc" - testkeyring "github.com/cosmos/evm/testutil/integration/os/keyring" - erc20mocks "github.com/cosmos/evm/x/erc20/types/mocks" + "github.com/cosmos/evm/testutil/integration/evm/factory" + "github.com/cosmos/evm/testutil/integration/evm/grpc" + testkeyring "github.com/cosmos/evm/testutil/keyring" "github.com/cosmos/evm/x/vm/keeper" "github.com/cosmos/evm/x/vm/types" - "go.uber.org/mock/gomock" + "github.com/ethereum/go-ethereum/params" ) const ( @@ -25,8 +22,8 @@ const ( // The gas part on the name refers to the file name to not generate a duplicated test name func (suite *KeeperTestSuite) TestGasRefundGas() { // Create a txFactory - grpcHandler := grpc.NewIntegrationHandler(suite.network) - txFactory := factory.New(suite.network, grpcHandler) + grpcHandler := grpc.NewIntegrationHandler(suite.Network) + txFactory := factory.New(suite.Network, grpcHandler) // Create a core message to use for the test keyring := testkeyring.New(2) @@ -54,21 +51,21 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { name: "Refund the full value as no gas was used", leftoverGas: DefaultCoreMsgGasUsage, expectedRefund: sdk.NewCoins( - sdk.NewCoin(suite.network.GetBaseDenom(), sdkmath.NewInt(DefaultCoreMsgGasUsage*DefaultGasPrice)), + sdk.NewCoin(suite.Network.GetBaseDenom(), sdkmath.NewInt(DefaultCoreMsgGasUsage*DefaultGasPrice)), ), }, { name: "Refund half the value as half gas was used", leftoverGas: DefaultCoreMsgGasUsage / 2, expectedRefund: sdk.NewCoins( - sdk.NewCoin(suite.network.GetBaseDenom(), sdkmath.NewInt((DefaultCoreMsgGasUsage*DefaultGasPrice)/2)), + sdk.NewCoin(suite.Network.GetBaseDenom(), sdkmath.NewInt((DefaultCoreMsgGasUsage*DefaultGasPrice)/2)), ), }, { name: "No refund as no gas was left over used", leftoverGas: 0, expectedRefund: sdk.NewCoins( - sdk.NewCoin(suite.network.GetBaseDenom(), sdkmath.NewInt(0)), + sdk.NewCoin(suite.Network.GetBaseDenom(), sdkmath.NewInt(0)), ), }, { @@ -143,54 +140,28 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { for _, tc := range testCases { suite.Run(tc.name, func() { // Generate a cached context to not leak data between tests - ctx, _ := suite.network.GetContext().CacheContext() - - // Create a new controller for the mock - ctrl := gomock.NewController(suite.T()) - defer ctrl.Finish() + ctx, _ := suite.Network.GetContext().CacheContext() // Apply the malleate function to the context if tc.malleate != nil { ctx = tc.malleate(ctx) } - // Create a new mock bank keeper - mockBankKeeper := erc20mocks.NewMockBankKeeper(ctrl) - - // Apply the expect, but only if expected refund is not zero - if !tc.expectedRefund.IsZero() { - mockBankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx sdk.Context, senderModule string, recipient sdk.AccAddress, coins sdk.Coins) error { - if !coins.Equal(tc.expectedRefund) { - suite.T().Errorf("expected %s, got %s", tc.expectedRefund, coins) - } + vmdb := suite.Network.GetStateDB() + vmdb.AddRefund(params.TxGas) - return nil - }) + if tc.leftoverGas > DefaultCoreMsgGasUsage { + return } - // Initialize a new EVM keeper with the mock bank keeper - // We need to redo this every time, since we will apply the mocked bank keeper at this step - evmKeeper := keeper.NewKeeper( - suite.network.App.AppCodec(), - suite.network.App.GetKey(types.StoreKey), - suite.network.App.GetTKey(types.StoreKey), - authtypes.NewModuleAddress(govtypes.ModuleName), - suite.network.App.AccountKeeper, - mockBankKeeper, - suite.network.App.StakingKeeper, - suite.network.App.FeeMarketKeeper, - suite.network.App.Erc20Keeper, - "", - suite.network.App.GetSubspace(types.ModuleName), - ) + gasUsed := DefaultCoreMsgGasUsage - tc.leftoverGas - // Call the msg, not further checks are needed, all balance checks are done in the mock - err := evmKeeper.RefundGas( - ctx, - coreMsg, + err = suite.Network.App.GetEVMKeeper().RefundGas( + suite.Network.GetContext(), + *coreMsg, tc.leftoverGas, - suite.network.GetBaseDenom(), + gasUsed, + suite.Network.GetBaseDenom(), ) // Check the error diff --git a/tests/integration/x/vm/test_state_transition.go b/tests/integration/x/vm/test_state_transition.go index f44f5d5e..450d8ce5 100644 --- a/tests/integration/x/vm/test_state_transition.go +++ b/tests/integration/x/vm/test_state_transition.go @@ -488,6 +488,7 @@ func (s *KeeperTestSuite) TestRefundGas() { unitNetwork.GetContext(), *coreMsg, refund, + gasUsed, unitNetwork.GetBaseDenom(), ) diff --git a/x/vm/keeper/gas.go b/x/vm/keeper/gas.go index be539ad5..c7bdd5f4 100644 --- a/x/vm/keeper/gas.go +++ b/x/vm/keeper/gas.go @@ -35,12 +35,12 @@ func (k *Keeper) GetEthIntrinsicGas(ctx sdk.Context, msg core.Message, cfg *para // consumed in the transaction. Additionally, the function sets the total gas consumed to the value // returned by the EVM execution, thus ignoring the previous intrinsic gas consumed during in the // AnteHandler. -func (k *Keeper) RefundGas(ctx sdk.Context, msg core.Message, leftoverGas uint64, denom string) error { +func (k *Keeper) RefundGas(ctx sdk.Context, msg core.Message, leftoverGas uint64, gasUsed uint64, denom string) error { // Return EVM tokens for remaining gas, exchanged at the original rate. remaining := new(big.Int).Mul(new(big.Int).SetUint64(leftoverGas), msg.GasPrice) // Check if gas is zero - if msg.Gas() == 0 { + if gasUsed == 0 { // If gas is zero, we cannot refund anything, so we return early return nil } @@ -72,7 +72,7 @@ func (k *Keeper) RefundGas(ctx sdk.Context, msg core.Message, leftoverGas uint64 // remaining = amount * leftoverGas / gasUsed remaining = new(big.Int).Div( new(big.Int).Mul(amount, new(big.Int).SetUint64(leftoverGas)), - new(big.Int).SetUint64(msg.Gas()), + new(big.Int).SetUint64(gasUsed), ) } } diff --git a/x/vm/keeper/state_transition.go b/x/vm/keeper/state_transition.go index eb8975cf..8cce23b8 100644 --- a/x/vm/keeper/state_transition.go +++ b/x/vm/keeper/state_transition.go @@ -311,7 +311,7 @@ func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (*t if msg.GasLimit > res.GasUsed { remainingGas = msg.GasLimit - res.GasUsed } - if err = k.RefundGas(ctx, *msg, remainingGas, types.GetEVMCoinDenom()); err != nil { + if err = k.RefundGas(ctx, *msg, remainingGas, res.GasUsed, types.GetEVMCoinDenom()); err != nil { return nil, errorsmod.Wrapf(err, "failed to refund gas leftover gas to sender %s", msg.From) } From f7305fa9a47ba56ae6af2a2b88daff3ef3d26855 Mon Sep 17 00:00:00 2001 From: Thales Zirbel Date: Mon, 22 Dec 2025 11:06:10 -0300 Subject: [PATCH 06/12] fix: use correct network type for test --- tests/integration/x/vm/test_gas.go | 53 +++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/tests/integration/x/vm/test_gas.go b/tests/integration/x/vm/test_gas.go index dbe2e7e3..a1ea69f3 100644 --- a/tests/integration/x/vm/test_gas.go +++ b/tests/integration/x/vm/test_gas.go @@ -5,8 +5,11 @@ import ( sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/evm/testutil/integration/evm/factory" "github.com/cosmos/evm/testutil/integration/evm/grpc" + "github.com/cosmos/evm/testutil/integration/evm/network" testkeyring "github.com/cosmos/evm/testutil/keyring" "github.com/cosmos/evm/x/vm/keeper" "github.com/cosmos/evm/x/vm/types" @@ -21,12 +24,38 @@ const ( // TestGasRefundGas tests the refund gas exclusively without going though the state transition // The gas part on the name refers to the file name to not generate a duplicated test name func (suite *KeeperTestSuite) TestGasRefundGas() { + // FeeCollector account is pre-funded with enough tokens + // for refund to work + // NOTE: everything should happen within the same block for + // feecollector account to remain funded + baseDenom := types.GetEVMCoinDenom() + + coins := sdk.NewCoins(sdk.NewCoin( + baseDenom, + sdkmath.NewInt(6e18), + )) + balances := []banktypes.Balance{ + { + Address: authtypes.NewModuleAddress(authtypes.FeeCollectorName).String(), + Coins: coins, + }, + } + bankGenesis := banktypes.DefaultGenesisState() + bankGenesis.Balances = balances + customGenesis := network.CustomGenesisState{} + customGenesis[banktypes.ModuleName] = bankGenesis + // Create a txFactory - grpcHandler := grpc.NewIntegrationHandler(suite.Network) - txFactory := factory.New(suite.Network, grpcHandler) + keyring := testkeyring.New(2) + unitNetwork := network.NewUnitTestNetwork( + suite.Create, + network.WithPreFundedAccounts(keyring.GetAllAccAddrs()...), + network.WithCustomGenesis(customGenesis), + ) + grpcHandler := grpc.NewIntegrationHandler(unitNetwork) + txFactory := factory.New(unitNetwork, grpcHandler) // Create a core message to use for the test - keyring := testkeyring.New(2) sender := keyring.GetKey(0) recipient := keyring.GetAddr(1) coreMsg, err := txFactory.GenerateGethCoreMsg( @@ -34,7 +63,7 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { types.EvmTxArgs{ To: &recipient, Amount: big.NewInt(100), - GasPrice: big.NewInt(120000), + GasPrice: big.NewInt(DefaultGasPrice), }, ) suite.Require().NoError(err) @@ -51,21 +80,21 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { name: "Refund the full value as no gas was used", leftoverGas: DefaultCoreMsgGasUsage, expectedRefund: sdk.NewCoins( - sdk.NewCoin(suite.Network.GetBaseDenom(), sdkmath.NewInt(DefaultCoreMsgGasUsage*DefaultGasPrice)), + sdk.NewCoin(baseDenom, sdkmath.NewInt(DefaultCoreMsgGasUsage*DefaultGasPrice)), ), }, { name: "Refund half the value as half gas was used", leftoverGas: DefaultCoreMsgGasUsage / 2, expectedRefund: sdk.NewCoins( - sdk.NewCoin(suite.Network.GetBaseDenom(), sdkmath.NewInt((DefaultCoreMsgGasUsage*DefaultGasPrice)/2)), + sdk.NewCoin(baseDenom, sdkmath.NewInt((DefaultCoreMsgGasUsage*DefaultGasPrice)/2)), ), }, { name: "No refund as no gas was left over used", leftoverGas: 0, expectedRefund: sdk.NewCoins( - sdk.NewCoin(suite.Network.GetBaseDenom(), sdkmath.NewInt(0)), + sdk.NewCoin(baseDenom, sdkmath.NewInt(0)), ), }, { @@ -140,14 +169,14 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { for _, tc := range testCases { suite.Run(tc.name, func() { // Generate a cached context to not leak data between tests - ctx, _ := suite.Network.GetContext().CacheContext() + ctx, _ := unitNetwork.GetContext().CacheContext() // Apply the malleate function to the context if tc.malleate != nil { ctx = tc.malleate(ctx) } - vmdb := suite.Network.GetStateDB() + vmdb := unitNetwork.GetStateDB() vmdb.AddRefund(params.TxGas) if tc.leftoverGas > DefaultCoreMsgGasUsage { @@ -156,12 +185,12 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { gasUsed := DefaultCoreMsgGasUsage - tc.leftoverGas - err = suite.Network.App.GetEVMKeeper().RefundGas( - suite.Network.GetContext(), + err = unitNetwork.App.GetEVMKeeper().RefundGas( + unitNetwork.GetContext(), *coreMsg, tc.leftoverGas, gasUsed, - suite.Network.GetBaseDenom(), + baseDenom, ) // Check the error From 987fef53858338ff1807599fcdfd0a62468c2860 Mon Sep 17 00:00:00 2001 From: Thales Zirbel Date: Mon, 22 Dec 2025 11:25:10 -0300 Subject: [PATCH 07/12] feat: use correct malleate and unify testdenom --- tests/integration/x/vm/test_gas.go | 39 ++++++++++++++++++------------ 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/tests/integration/x/vm/test_gas.go b/tests/integration/x/vm/test_gas.go index a1ea69f3..2a57a038 100644 --- a/tests/integration/x/vm/test_gas.go +++ b/tests/integration/x/vm/test_gas.go @@ -19,6 +19,7 @@ import ( const ( DefaultCoreMsgGasUsage = 21000 DefaultGasPrice = 120000 + TestDenom = "acoin" ) // TestGasRefundGas tests the refund gas exclusively without going though the state transition @@ -30,10 +31,16 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { // feecollector account to remain funded baseDenom := types.GetEVMCoinDenom() - coins := sdk.NewCoins(sdk.NewCoin( - baseDenom, - sdkmath.NewInt(6e18), - )) + coins := sdk.NewCoins( + sdk.NewCoin( + baseDenom, + sdkmath.NewInt(6e18), + ), + sdk.NewCoin( + TestDenom, + sdkmath.NewInt(6e18), + ), + ) balances := []banktypes.Balance{ { Address: authtypes.NewModuleAddress(authtypes.FeeCollectorName).String(), @@ -98,19 +105,19 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { ), }, { - name: "Refund with context fees, refunding the full value", - leftoverGas: DefaultCoreMsgGasUsage, + name: "Refund with context fees, refunding the almost full value", + leftoverGas: DefaultCoreMsgGasUsage - 1, malleate: func(ctx sdk.Context) sdk.Context { // Set the fee abstraction paid fee key with a single coin return ctx.WithValue( keeper.ContextPaidFeesKey{}, sdk.NewCoins( - sdk.NewCoin("acoin", sdkmath.NewInt(750_000_000)), + sdk.NewCoin(TestDenom, sdkmath.NewInt(750_000_000)), ), ) }, expectedRefund: sdk.NewCoins( - sdk.NewCoin("acoin", sdkmath.NewInt(750_000_000)), + sdk.NewCoin(TestDenom, sdkmath.NewInt(750_000_000)), ), }, { @@ -121,28 +128,28 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { return ctx.WithValue( keeper.ContextPaidFeesKey{}, sdk.NewCoins( - sdk.NewCoin("acoin", sdkmath.NewInt(750_000_000)), + sdk.NewCoin(TestDenom, sdkmath.NewInt(750_000_000)), ), ) }, expectedRefund: sdk.NewCoins( - sdk.NewCoin("acoin", sdkmath.NewInt(750_000_000/2)), + sdk.NewCoin(TestDenom, sdkmath.NewInt(750_000_000/2)), ), }, { name: "Refund with context fees, no refund", - leftoverGas: 0, + leftoverGas: 1, malleate: func(ctx sdk.Context) sdk.Context { // Set the fee abstraction paid fee key with a single coin return ctx.WithValue( keeper.ContextPaidFeesKey{}, sdk.NewCoins( - sdk.NewCoin("acoin", sdkmath.NewInt(750_000_000)), + sdk.NewCoin(TestDenom, sdkmath.NewInt(750_000_000)), ), ) }, expectedRefund: sdk.NewCoins( - sdk.NewCoin("acoin", sdkmath.NewInt(0)), + sdk.NewCoin(TestDenom, sdkmath.NewInt(0)), ), }, { @@ -153,13 +160,13 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { return ctx.WithValue( keeper.ContextPaidFeesKey{}, sdk.NewCoins( - sdk.NewCoin("acoin", sdkmath.NewInt(750_000_000)), + sdk.NewCoin(TestDenom, sdkmath.NewInt(750_000_000)), sdk.NewCoin("atwo", sdkmath.NewInt(750_000_000)), ), ) }, expectedRefund: sdk.NewCoins( - sdk.NewCoin("acoin", sdkmath.NewInt(0)), // We say as zero to skip the mock bank check + sdk.NewCoin(TestDenom, sdkmath.NewInt(0)), // We say as zero to skip the mock bank check ), errContains: "expected a single coin for EVM refunds, got 2", }, @@ -186,7 +193,7 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { gasUsed := DefaultCoreMsgGasUsage - tc.leftoverGas err = unitNetwork.App.GetEVMKeeper().RefundGas( - unitNetwork.GetContext(), + ctx, *coreMsg, tc.leftoverGas, gasUsed, From 77780c8a66d10141ef06d721f721a938ccf6e2e4 Mon Sep 17 00:00:00 2001 From: Thales Zirbel Date: Mon, 22 Dec 2025 11:26:35 -0300 Subject: [PATCH 08/12] fix: correct double coin case --- tests/integration/x/vm/test_gas.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/x/vm/test_gas.go b/tests/integration/x/vm/test_gas.go index 2a57a038..6a50cab2 100644 --- a/tests/integration/x/vm/test_gas.go +++ b/tests/integration/x/vm/test_gas.go @@ -154,7 +154,7 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { }, { name: "Error - More than one coin being passed", - leftoverGas: DefaultCoreMsgGasUsage, + leftoverGas: DefaultCoreMsgGasUsage - 1, // Using some leftover so the refund doesn't short circuit malleate: func(ctx sdk.Context) sdk.Context { // Set the fee abstraction paid fee key with a single coin return ctx.WithValue( From c602fbfcd6f1b9120410b09aa2952054595c2158 Mon Sep 17 00:00:00 2001 From: Thales Zirbel Date: Mon, 22 Dec 2025 15:47:47 -0300 Subject: [PATCH 09/12] feat: use correct gas used --- tests/integration/x/vm/test_gas.go | 39 +++++++++++++++++++----------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/tests/integration/x/vm/test_gas.go b/tests/integration/x/vm/test_gas.go index 6a50cab2..1213bec0 100644 --- a/tests/integration/x/vm/test_gas.go +++ b/tests/integration/x/vm/test_gas.go @@ -3,17 +3,20 @@ package vm import ( "math/big" - sdkmath "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/ethereum/go-ethereum/params" + "github.com/cosmos/evm/testutil/integration/evm/factory" "github.com/cosmos/evm/testutil/integration/evm/grpc" "github.com/cosmos/evm/testutil/integration/evm/network" testkeyring "github.com/cosmos/evm/testutil/keyring" "github.com/cosmos/evm/x/vm/keeper" "github.com/cosmos/evm/x/vm/types" - "github.com/ethereum/go-ethereum/params" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) const ( @@ -41,9 +44,11 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { sdkmath.NewInt(6e18), ), ) + feeAddress := authtypes.NewModuleAddress(authtypes.FeeCollectorName) + balances := []banktypes.Balance{ { - Address: authtypes.NewModuleAddress(authtypes.FeeCollectorName).String(), + Address: feeAddress.String(), Coins: coins, }, } @@ -105,8 +110,8 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { ), }, { - name: "Refund with context fees, refunding the almost full value", - leftoverGas: DefaultCoreMsgGasUsage - 1, + name: "Refund with context fees, refunding the full value", + leftoverGas: DefaultCoreMsgGasUsage, malleate: func(ctx sdk.Context) sdk.Context { // Set the fee abstraction paid fee key with a single coin return ctx.WithValue( @@ -165,9 +170,6 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { ), ) }, - expectedRefund: sdk.NewCoins( - sdk.NewCoin(TestDenom, sdkmath.NewInt(0)), // We say as zero to skip the mock bank check - ), errContains: "expected a single coin for EVM refunds, got 2", }, } @@ -190,13 +192,15 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { return } - gasUsed := DefaultCoreMsgGasUsage - tc.leftoverGas + initialBalances := unitNetwork.App.GetBankKeeper().GetAllBalances(ctx, feeAddress) + + // gasUsed := DefaultCoreMsgGasUsage - tc.leftoverGas err = unitNetwork.App.GetEVMKeeper().RefundGas( ctx, *coreMsg, tc.leftoverGas, - gasUsed, + DefaultCoreMsgGasUsage, baseDenom, ) @@ -206,7 +210,14 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { } else { suite.Require().NoError(err, "RefundGas should not return an error") } + + // Check the balance change + if !tc.expectedRefund.Empty() { + diff := initialBalances.Sub(unitNetwork.App.GetBankKeeper().GetAllBalances(ctx, feeAddress)...) + for _, coin := range tc.expectedRefund { + suite.Require().Equal(coin.Amount, diff.AmountOf(coin.Denom)) + } + } }) } - } From 1d80bd3a74311cf9e1c818f75acef879c9e6cfba Mon Sep 17 00:00:00 2001 From: Thales Zirbel Date: Mon, 22 Dec 2025 15:49:39 -0300 Subject: [PATCH 10/12] chore: remove commented code --- tests/integration/x/vm/test_gas.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integration/x/vm/test_gas.go b/tests/integration/x/vm/test_gas.go index 1213bec0..d3b99cc9 100644 --- a/tests/integration/x/vm/test_gas.go +++ b/tests/integration/x/vm/test_gas.go @@ -194,8 +194,6 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { initialBalances := unitNetwork.App.GetBankKeeper().GetAllBalances(ctx, feeAddress) - // gasUsed := DefaultCoreMsgGasUsage - tc.leftoverGas - err = unitNetwork.App.GetEVMKeeper().RefundGas( ctx, *coreMsg, From 15fe9a2eb880ceb42b7868fd283ad56af86da28c Mon Sep 17 00:00:00 2001 From: Thales Zirbel Date: Mon, 22 Dec 2025 16:17:08 -0300 Subject: [PATCH 11/12] fix: revert uneeded changes --- tests/integration/x/vm/test_gas.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/integration/x/vm/test_gas.go b/tests/integration/x/vm/test_gas.go index d3b99cc9..156455f9 100644 --- a/tests/integration/x/vm/test_gas.go +++ b/tests/integration/x/vm/test_gas.go @@ -3,8 +3,6 @@ package vm import ( "math/big" - "github.com/ethereum/go-ethereum/params" - "github.com/cosmos/evm/testutil/integration/evm/factory" "github.com/cosmos/evm/testutil/integration/evm/grpc" "github.com/cosmos/evm/testutil/integration/evm/network" @@ -143,7 +141,7 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { }, { name: "Refund with context fees, no refund", - leftoverGas: 1, + leftoverGas: 0, malleate: func(ctx sdk.Context) sdk.Context { // Set the fee abstraction paid fee key with a single coin return ctx.WithValue( @@ -159,7 +157,7 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { }, { name: "Error - More than one coin being passed", - leftoverGas: DefaultCoreMsgGasUsage - 1, // Using some leftover so the refund doesn't short circuit + leftoverGas: DefaultCoreMsgGasUsage, malleate: func(ctx sdk.Context) sdk.Context { // Set the fee abstraction paid fee key with a single coin return ctx.WithValue( @@ -185,8 +183,8 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { ctx = tc.malleate(ctx) } - vmdb := unitNetwork.GetStateDB() - vmdb.AddRefund(params.TxGas) + // vmdb := unitNetwork.GetStateDB() + // vmdb.AddRefund(DefaultCoreMsgGasUsage) if tc.leftoverGas > DefaultCoreMsgGasUsage { return From 12bde30b6eaaa52cffc785c4a231db5757d35d91 Mon Sep 17 00:00:00 2001 From: Thales Zirbel Date: Mon, 22 Dec 2025 16:20:26 -0300 Subject: [PATCH 12/12] fix: remove uneeded comment --- tests/integration/x/vm/test_gas.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/integration/x/vm/test_gas.go b/tests/integration/x/vm/test_gas.go index 156455f9..a3e56362 100644 --- a/tests/integration/x/vm/test_gas.go +++ b/tests/integration/x/vm/test_gas.go @@ -183,9 +183,6 @@ func (suite *KeeperTestSuite) TestGasRefundGas() { ctx = tc.malleate(ctx) } - // vmdb := unitNetwork.GetStateDB() - // vmdb.AddRefund(DefaultCoreMsgGasUsage) - if tc.leftoverGas > DefaultCoreMsgGasUsage { return }