Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* (app) [#852](https://github.com/crypto-org-chain/ethermint/pull/852) Add interface implementation for evm and feemarket module.
* (fix) [#878](https://github.com/crypto-org-chain/ethermint/pull/878) Patch default history serve window with correct value.
* (rpc) [#916](https://github.com/crypto-org-chain/ethermint/pull/916) fix(rpc): return InvalidParams error when `eth_getLogs` toBlock exceeds chain head.
* (evm) [#921](https://github.com/crypto-org-chain/ethermint/pull/921) fix: enforce floor-data-gas
Comment thread
XinyuCRO marked this conversation as resolved.

## [v0.23.0] - 2026-01-13

Expand Down
8 changes: 4 additions & 4 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 11 additions & 4 deletions x/evm/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,10 @@ func (suite *HandlerTestSuite) deployERC20Contract() common.Address {
// - when transaction reverted, gas refund works.
// - when transaction reverted, nonce is still increased.
func (suite *HandlerTestSuite) TestERC20TransferReverted() {
intrinsicGas := uint64(21572)
transferData, err := types.ERC20Contract.ABI.Pack("transfer", suite.Address, big.NewInt(10))
suite.Require().NoError(err)
intrinsicGas, err := core.FloorDataGas(transferData)
Comment thread
XinyuCRO marked this conversation as resolved.
Outdated
suite.Require().NoError(err)
// test different hooks scenarios
testCases := []struct {
msg string
Expand Down Expand Up @@ -484,8 +487,8 @@ func (suite *HandlerTestSuite) TestERC20TransferReverted() {

rules := params.Rules{
IsHomestead: true,
IsIstanbul: true,
IsShanghai: true,
IsIstanbul: true,
IsShanghai: true,
}
fees, err := keeper.VerifyFee(tx, "aphoton", baseFee, rules, suite.Ctx.IsCheckTx())
suite.Require().NoError(err)
Expand Down Expand Up @@ -518,7 +521,11 @@ func (suite *HandlerTestSuite) TestERC20TransferReverted() {
}

func (suite *HandlerTestSuite) TestContractDeploymentRevert() {
intrinsicGas := uint64(134510)
ctorArgsForFloor, err := types.ERC20Contract.ABI.Pack("", suite.Address, big.NewInt(0))
suite.Require().NoError(err)
deployData := append(types.ERC20Contract.Bin, ctorArgsForFloor...)
intrinsicGas, err := core.FloorDataGas(deployData)
suite.Require().NoError(err)
testCases := []struct {
msg string
gasLimit uint64
Expand Down
53 changes: 53 additions & 0 deletions x/evm/keeper/integration_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package keeper_test

import (
"bytes"
"math/big"
"testing"

Expand Down Expand Up @@ -119,6 +120,45 @@ var _ = Describe("Evm", func() {
)
})
})

// EIP-7623: Prague calldata floor gas must be enforced in both CheckTx and DeliverTx.
//
// For 1024 non-zero calldata bytes:
// intrinsicGas = 21000 + 16 * 1024 = 37384
// floorDataGas = 21000 + 10 * 4 * 1024 = 61960
//
// A transaction with gasLimit = intrinsicGas is below the floor and must be rejected
// in both CheckTx (mempool admission) and DeliverTx (block execution).
Context("EIP-7623 Prague calldata floor gas", func() {
const (
intrinsicGas = uint64(21000 + 16*1024) // 37384
floorDataGas = uint64(21000 + 10*4*1024) // 61960
)

BeforeEach(func() {
setupTest(sdkmath.LegacyZeroDec(), big.NewInt(0))
})

It("CheckTx rejects gasLimit below floor", func() {
txBz := prepareFloorDataGasTx(intrinsicGas)
res := s.CheckTx(txBz)
Expect(res.IsOK()).To(BeFalse(), "CheckTx must reject below-floor Prague tx, got log: %s", res.GetLog())
})

It("DeliverTx rejects gasLimit below floor (fix validates block execution path)", func() {
txBz := prepareFloorDataGasTx(intrinsicGas)
res := s.DeliverTx(txBz)
Expect(res.IsOK()).To(BeFalse(),
"DeliverTx must also reject below-floor Prague tx, got log: %s", res.GetLog())
})

It("DeliverTx accepts gasLimit at floor", func() {
txBz := prepareFloorDataGasTx(floorDataGas)
res := s.DeliverTx(txBz)
Expect(res.IsOK()).To(BeTrue(),
"DeliverTx must accept gasLimit == floorDataGas, got log: %s", res.GetLog())
})
})
})
})

Expand Down Expand Up @@ -158,3 +198,16 @@ func prepareEthTx(p txParams) []byte {
msg := s.BuildEthTx(&to, p.gasLimit, p.gasPrice, p.gasFeeCap, p.gasTipCap, p.accesses, s.PrivKey)
return s.PrepareEthTx(msg, s.PrivKey)
}

// prepareFloorDataGasTx builds a legacy tx with 1024 non-zero calldata bytes and the
// given gasLimit, signed with the test account. It does NOT use BuildEthTx because that
// helper does not accept calldata.
func prepareFloorDataGasTx(gasLimit uint64) []byte {
calldata := bytes.Repeat([]byte{0xff}, 1024)
chainID := s.App.EvmKeeper.ChainID()
nonce := s.App.EvmKeeper.GetNonce(s.Ctx, s.Address)
to := tests.GenerateAddress()
msg := evmtypes.NewTx(chainID, nonce, &to, nil, gasLimit, big.NewInt(0), nil, nil, calldata, nil)
msg.From = s.Address.Bytes()
return s.PrepareEthTx(msg, s.PrivKey)
}
8 changes: 8 additions & 0 deletions x/evm/keeper/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
v5 "github.com/evmos/ethermint/x/evm/migrations/v5"
v6 "github.com/evmos/ethermint/x/evm/migrations/v6"
v7 "github.com/evmos/ethermint/x/evm/migrations/v7"
v8 "github.com/evmos/ethermint/x/evm/migrations/v8"
"github.com/evmos/ethermint/x/evm/types"
)

Expand Down Expand Up @@ -57,3 +58,10 @@ func (m Migrator) Migrate5to6(ctx sdk.Context) error {
func (m Migrator) Migrate6to7(ctx sdk.Context) error {
return v7.MigrateStore(ctx, m.keeper.storeKey, m.keeper.cdc)
}

// Migrate7to8 migrates the store from consensus version 7 to 8.
// This version enforces EIP-7623 floor data gas in all execution contexts.
// No on-chain state change is required; the bump signals a consensus-breaking update.
func (m Migrator) Migrate7to8(ctx sdk.Context) error {
return v8.MigrateStore(ctx)
}
22 changes: 22 additions & 0 deletions x/evm/keeper/simulate.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,17 @@ func (sim *Simulator) applyCall(
}
leftoverGas -= intrinsicGas

// Enforce EIP-7623 floor data gas for Prague (mirrors ApplyMessageWithConfig).
if rules.IsPrague {
floorDataGas, err := core.FloorDataGas(msg.Data)
if err != nil {
return applyCallResult{}, err
}
if msg.GasLimit < floorDataGas {
return applyCallResult{}, core.ErrFloorDataGas
}
}

// Shanghai init code size check
if rules.IsShanghai && contractCreation && len(msg.Data) > params.MaxInitCodeSize {
return applyCallResult{}, fmt.Errorf("%w: code size %v limit %v", core.ErrMaxInitCodeSizeExceeded, len(msg.Data), params.MaxInitCodeSize)
Expand Down Expand Up @@ -408,6 +419,17 @@ func (sim *Simulator) applyCall(
refund := GasToRefund(sim.state.GetRefund(), temporaryGasUsed, refundQuotient)
leftoverGas += refund

// Apply EIP-7623 post-execution floor on post-refund gas used.
if rules.IsPrague {
floorDataGas, err := core.FloorDataGas(msg.Data)
if err != nil {
return applyCallResult{}, err
}
if msg.GasLimit-leftoverGas < floorDataGas {
leftoverGas = msg.GasLimit - floorDataGas
Comment thread
XinyuCRO marked this conversation as resolved.
}
Comment thread
XinyuCRO marked this conversation as resolved.
}

gasUsed := msg.GasLimit - leftoverGas

// charge gas
Expand Down
27 changes: 27 additions & 0 deletions x/evm/keeper/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,19 @@ func (k *Keeper) ApplyMessageWithConfig(
tracer.OnGasChange(msg.GasLimit, leftoverGas, tracing.GasChangeTxIntrinsicGas)
}

// Enforce EIP-7623 floor data gas for Prague in all execution contexts,
// including eth_call and FinalizeBlock paths that bypass the ante handler.
if rules.IsPrague {
floorDataGas, err := core.FloorDataGas(msg.Data)
if err != nil {
return nil, errorsmod.Wrap(err, "floor data gas")
}
if msg.GasLimit < floorDataGas {
return nil, errorsmod.Wrapf(core.ErrFloorDataGas,
"gas %d, minimum needed %d", msg.GasLimit, floorDataGas)
}
}

// access list preparation is moved from ante handler to here, because it's needed when `ApplyMessage` is called
// under contexts where ante handlers are not run, for example `eth_call` and `eth_estimateGas`.
// Check whether the init code size has been exceeded.
Expand Down Expand Up @@ -475,6 +488,20 @@ func (k *Keeper) ApplyMessageWithConfig(
tracer.OnGasChange(leftoverGas-refund, leftoverGas, tracing.GasChangeTxRefunds)
}

// Apply EIP-7623 post-execution floor: enforce on post-refund gas used.
// leftoverGas already includes the refund at this point, so
// (msg.GasLimit - leftoverGas) is the post-refund gas used.
if rules.IsPrague {
floorDataGas, err := core.FloorDataGas(msg.Data)
if err != nil {
return nil, errorsmod.Wrap(err, "floor data gas")
}
if msg.GasLimit-leftoverGas < floorDataGas {
leftoverGas = msg.GasLimit - floorDataGas
Comment thread
XinyuCRO marked this conversation as resolved.
}
Comment thread
XinyuCRO marked this conversation as resolved.
}
temporaryGasUsed = msg.GasLimit - leftoverGas

// EVM execution error needs to be available for the JSON-RPC client
var vmError string
if vmErr != nil {
Expand Down
86 changes: 86 additions & 0 deletions x/evm/keeper/state_transition_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package keeper_test

import (
"bytes"
"fmt"
"math"
"math/big"
Expand Down Expand Up @@ -865,3 +866,88 @@ func (suite *StateTransitionTestSuite) TestBlobBaseFeeOpcode() {
suite.Require().Equal(expected, result.Ret, "BLOBBASEFEE should return overridden value 42")
})
}

// TestPragueFloorDataGas verifies EIP-7623 floor data gas enforcement in ApplyMessageWithConfig.
// A transaction whose gasLimit >= intrinsicGas but < floorDataGas must be rejected,
// and when gasLimit >= floorDataGas the charged gas must be at least floorDataGas.
func (suite *StateTransitionTestSuite) TestPragueFloorDataGas() {
suite.SetupTest()

// 1024 non-zero calldata bytes:
// intrinsicGas = 21000 + 16 * 1024 = 37384
// floorDataGas = 21000 + 10 * (4 * 1024) = 61960
calldata := bytes.Repeat([]byte{0xff}, 1024)

intrinsicGas := uint64(21000 + 16*1024) // 37384
Comment thread
XinyuCRO marked this conversation as resolved.
Outdated
floorDataGas := uint64(21000 + 10*4*1024) // 61960
suite.Require().Less(intrinsicGas, floorDataGas, "test invariant: floor > intrinsic")

to := suite.Address
cfg, err := suite.App.EvmKeeper.EVMConfig(suite.Ctx, suite.App.EvmKeeper.ChainID(), common.Hash{})
suite.Require().NoError(err)
cfg.TxConfig = suite.App.EvmKeeper.TxConfig(suite.Ctx, common.Hash{})

suite.Require().True(cfg.Rules.IsPrague, "Prague must be active for this test")

suite.Run("rejects gasLimit below floor", func() {
msg := &core.Message{
To: &to,
From: suite.Address,
Nonce: suite.StateDB().GetNonce(suite.Address),
Value: big.NewInt(0),
GasLimit: intrinsicGas, // valid vs intrinsicGas, invalid vs floorDataGas
GasPrice: big.NewInt(0),
GasFeeCap: big.NewInt(0),
GasTipCap: big.NewInt(0),
Data: calldata,
SkipNonceChecks: true,
}

_, err := suite.App.EvmKeeper.ApplyMessageWithConfig(suite.Ctx, msg, cfg, true)
suite.Require().Error(err, "must reject gasLimit < floorDataGas under Prague")
suite.Require().Contains(err.Error(), "floor data gas")
})

suite.Run("accepts gasLimit at floor and charges floor gas", func() {
msg := &core.Message{
To: &to,
From: suite.Address,
Nonce: suite.StateDB().GetNonce(suite.Address),
Value: big.NewInt(0),
GasLimit: floorDataGas, // exactly at floor
GasPrice: big.NewInt(0),
GasFeeCap: big.NewInt(0),
GasTipCap: big.NewInt(0),
Data: calldata,
SkipNonceChecks: true,
}

result, err := suite.App.EvmKeeper.ApplyMessageWithConfig(suite.Ctx, msg, cfg, true)
suite.Require().NoError(err)
suite.Require().False(result.Failed())
// Charged gas must equal floorDataGas even though actual EVM execution cost < floor.
suite.Require().GreaterOrEqual(result.GasUsed, floorDataGas,
"gasUsed must be at least floorDataGas under Prague EIP-7623")
})

suite.Run("accepts gasLimit above floor and charges at least floor gas", func() {
msg := &core.Message{
To: &to,
From: suite.Address,
Nonce: suite.StateDB().GetNonce(suite.Address),
Value: big.NewInt(0),
GasLimit: floorDataGas * 2, // well above floor
GasPrice: big.NewInt(0),
GasFeeCap: big.NewInt(0),
GasTipCap: big.NewInt(0),
Data: calldata,
SkipNonceChecks: true,
}

result, err := suite.App.EvmKeeper.ApplyMessageWithConfig(suite.Ctx, msg, cfg, true)
suite.Require().NoError(err)
suite.Require().False(result.Failed())
suite.Require().GreaterOrEqual(result.GasUsed, floorDataGas,
"gasUsed must be at least floorDataGas under Prague EIP-7623")
})
}
2 changes: 1 addition & 1 deletion x/evm/keeper/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func VerifyFee(
}

// Gas limit suffices for the floor data cost (EIP-7623)
if isCheckTx && rules.IsPrague {
if rules.IsPrague {
floorDataGas, err := core.FloorDataGas(tx.Data())
if err != nil {
return nil, err
Expand Down
13 changes: 13 additions & 0 deletions x/evm/migrations/v8/migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package v8

import (
sdk "github.com/cosmos/cosmos-sdk/types"
)

// MigrateStore migrates the x/evm module state from consensus version 7 to 8.
// This version enforces EIP-7623 (Prague calldata floor gas) in all execution
// contexts, not just CheckTx. No on-chain state is modified by this migration;
// the version bump signals a consensus-breaking change in validation logic.
Comment thread
XinyuCRO marked this conversation as resolved.
Outdated
func MigrateStore(_ sdk.Context) error {
return nil
}
19 changes: 19 additions & 0 deletions x/evm/migrations/v8/migrate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package v8_test

import (
"testing"

storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/testutil"
v8 "github.com/evmos/ethermint/x/evm/migrations/v8"
"github.com/stretchr/testify/require"
)

func TestMigrateStore(t *testing.T) {
storeKey := storetypes.NewKVStoreKey("evm")
tKey := storetypes.NewTransientStoreKey("transient_test")
ctx := testutil.DefaultContext(storeKey, tKey)

// v8 migration is a no-op store change; it must always succeed.
require.NoError(t, v8.MigrateStore(ctx))
}
6 changes: 5 additions & 1 deletion x/evm/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {

// ConsensusVersion returns the consensus state-breaking version for the module.
func (AppModule) ConsensusVersion() uint64 {
return 7
return 8
}

// DefaultGenesis returns default genesis state as raw bytes for the evm
Expand Down Expand Up @@ -146,6 +146,10 @@ func (am AppModule) RegisterServices(cfg module.Configurator) {
if err := cfg.RegisterMigration(types.ModuleName, 6, m.Migrate6to7); err != nil {
panic(err)
}

if err := cfg.RegisterMigration(types.ModuleName, 7, m.Migrate7to8); err != nil {
panic(err)
}
}

// BeginBlock returns the begin block for the evm module.
Expand Down
Loading