Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions tests/integration/x/vm/test_gas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package vm

import (
"math/big"

"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"

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 (
DefaultCoreMsgGasUsage = 21000
DefaultGasPrice = 120000
TestDenom = "acoin"
)

// 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),
),
sdk.NewCoin(
TestDenom,
sdkmath.NewInt(6e18),
),
)
feeAddress := authtypes.NewModuleAddress(authtypes.FeeCollectorName)

balances := []banktypes.Balance{
{
Address: feeAddress.String(),
Coins: coins,
},
}
bankGenesis := banktypes.DefaultGenesisState()
bankGenesis.Balances = balances
customGenesis := network.CustomGenesisState{}
customGenesis[banktypes.ModuleName] = bankGenesis

// Create a txFactory
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
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(DefaultGasPrice),
},
)
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(baseDenom, sdkmath.NewInt(DefaultCoreMsgGasUsage*DefaultGasPrice)),
),
},
{
name: "Refund half the value as half gas was used",
leftoverGas: DefaultCoreMsgGasUsage / 2,
expectedRefund: sdk.NewCoins(
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(baseDenom, 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(TestDenom, sdkmath.NewInt(750_000_000)),
),
)
},
expectedRefund: sdk.NewCoins(
sdk.NewCoin(TestDenom, 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(TestDenom, sdkmath.NewInt(750_000_000)),
),
)
},
expectedRefund: sdk.NewCoins(
sdk.NewCoin(TestDenom, 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(TestDenom, sdkmath.NewInt(750_000_000)),
),
)
},
expectedRefund: sdk.NewCoins(
sdk.NewCoin(TestDenom, 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(TestDenom, sdkmath.NewInt(750_000_000)),
sdk.NewCoin("atwo", sdkmath.NewInt(750_000_000)),
),
)
},
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, _ := unitNetwork.GetContext().CacheContext()

// Apply the malleate function to the context
if tc.malleate != nil {
ctx = tc.malleate(ctx)
}

if tc.leftoverGas > DefaultCoreMsgGasUsage {
return
}

initialBalances := unitNetwork.App.GetBankKeeper().GetAllBalances(ctx, feeAddress)

err = unitNetwork.App.GetEVMKeeper().RefundGas(
ctx,
*coreMsg,
tc.leftoverGas,
DefaultCoreMsgGasUsage,
baseDenom,
)

// 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")
}

// 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))
}
}
})
}
}
1 change: 1 addition & 0 deletions tests/integration/x/vm/test_state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ func (s *KeeperTestSuite) TestRefundGas() {
unitNetwork.GetContext(),
*coreMsg,
refund,
gasUsed,
unitNetwork.GetBaseDenom(),
)

Expand Down
44 changes: 40 additions & 4 deletions x/vm/keeper/gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import (
authtypes "github.com/cosmos/cosmos-sdk/x/auth/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,
Expand All @@ -32,26 +35,59 @@ 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 gasUsed == 0 {
// If gas is zero, we cannot refund anything, so we return early
return nil
}

switch remaining.Sign() {
case -1:
// 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 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 {
// 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(gasUsed),
)
}
}

// 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
Expand Down
2 changes: 1 addition & 1 deletion x/vm/keeper/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Loading