Skip to main content
This migration contains breaking changes that require code updatesv0.5.0 introduces significant changes to precompile interfaces, VM parameters, mempool architecture, and ante handlers. Review all breaking changes before upgrading.

Breaking Changes Summary

New Features


0) Preparation

Create an upgrade branch and prepare your environment:
git switch -c upgrade/evm-v0.5
go test ./...
evmd export > pre-upgrade-genesis.json

1) Dependency Updates

Update go.mod

- github.com/cosmos/evm v0.4.1
+ github.com/cosmos/evm v0.5.0
go mod tidy

2) VM Parameter Changes

BREAKING: allow_unprotected_txs Parameter Removed

What it was: This parameter controlled whether to accept non-EIP-155 transactions (transactions without chain ID in signature). Why removed: Security concerns - non-EIP-155 transactions are vulnerable to replay attacks across chains. The v0.5.0 approach is to reject these by default, with node operators able to override via local configuration if absolutely necessary (not recommended). Migration Required:
  1. Update Genesis Files:
{
  "app_state": {
    "vm": {
      "params": {
        "evm_denom": "atest",
        "extra_eips": [],
-       "allow_unprotected_txs": false,
        "evm_channels": [],
        "access_control": {...},
        "active_static_precompiles": [...],
+       "history_serve_window": 8192
      }
    }
  }
}
  1. Update Parameter Validation:
If you have custom parameter validation logic, remove references to allow_unprotected_txs:
- if !params.AllowUnprotectedTxs {
-   return errors.New("unprotected transactions not allowed")
- }
Impact if not removed: Genesis validation will fail with “unknown field” error.

NEW: history_serve_window Parameter

Purpose: Implements EIP-2935 (historical block hashes in state) for better Ethereum compatibility. What it controls: Number of recent block hashes accessible via the BLOCKHASH opcode and eth_getBlockByNumber. Configuration guidance:
  • Default: 8192 blocks (~13 hours at 6s blocks)
  • Archive nodes: 43200 (~3 days)
  • Light clients: 256 (minimal history)
Storage impact: Each block hash uses ~100 bytes. 8192 blocks = ~800KB additional state. Why this matters: Smart contracts often verify historical data using block hashes. Without sufficient history, these verifications fail.

3) Precompile Interface Changes

BREAKING: Constructor Interface Updates

The Problem v0.5.0 Solves: In v0.4.0, precompiles were tightly coupled to concrete keeper implementations. This made testing difficult and created import cycles. The v0.5.0 approach uses interfaces for clean dependency injection. Impact: ALL precompile initializations must be updated. This is not optional.

What Changed and Why

Each precompile constructor now uses interface types from precompiles/common/interfaces.go:
// v0.4.0 - Concrete types (problematic)
func NewPrecompile(keeper bankkeeper.Keeper) (*Precompile, error)

// v0.5.0 - Interface types (clean)
func NewPrecompile(keeper common.BankKeeper) (*Precompile, error)
Benefits of this change:
  • Eliminates import cycles between modules
  • Enables proper mocking for tests
  • Reduces compilation time
  • Allows alternative keeper implementations

Migration Steps for Each Precompile

Bank Precompile

- bankPrecompile, err := bankprecompile.NewPrecompile(
-     app.BankKeeper,  // Direct concrete keeper
-     appCodec,
- )
+ bankPrecompile, err := bankprecompile.NewPrecompile(
+     common.BankKeeper(app.BankKeeper),  // Cast to interface
+     appCodec,
+ )
Why the cast is safe: Your BankKeeper already implements all required methods.

Distribution Precompile

- distributionPrecompile, err := distributionprecompile.NewPrecompile(
-     app.DistrKeeper,
-     app.StakingKeeper,  // REMOVED - not needed anymore
-     app.AuthzKeeper,    // REMOVED - not used
-     appCodec,
-     addressCodec,
- )
+ distributionPrecompile, err := distributionprecompile.NewPrecompile(
+     common.DistributionKeeper(app.DistrKeeper),
+     appCodec,
+     addressCodec,
+ )
Why parameters were removed:
  • StakingKeeper: Was only used for validator queries, now handled differently
  • AuthzKeeper: Never actually used in the precompile implementation

Staking Precompile

- stakingPrecompile, err := stakingprecompile.NewPrecompile(
-     app.StakingKeeper,
-     appCodec,
-     addressCodec,
- )
+ stakingPrecompile, err := stakingprecompile.NewPrecompile(
+     common.StakingKeeper(app.StakingKeeper),
+     appCodec,
+     addressCodec,
+ )

ICS20 Precompile (Most Changed)

- ics20Precompile, err := ics20precompile.NewPrecompile(
-     app.TransferKeeper,
-     app.ChannelKeeper,
-     app.BankKeeper,
-     app.StakingKeeper,
-     app.EVMKeeper,      // REMOVED - circular dependency!
-     appCodec,
-     addressCodec,
- )
+ ics20Precompile, err := ics20precompile.NewPrecompile(
+     common.BankKeeper(app.BankKeeper),         // Bank first now
+     common.StakingKeeper(app.StakingKeeper),
+     app.TransferKeeper,                        // Concrete still OK
+     app.ChannelKeeper,                         // Concrete still OK
+     appCodec,
+     addressCodec,
+ )
Critical change: EVMKeeper removed to break circular dependency. The precompile no longer needs direct EVM access.

Governance Precompile

- govPrecompile, err := govprecompile.NewPrecompile(
-     app.GovKeeper,
-     appCodec,
-     addressCodec,
- )
+ govPrecompile, err := govprecompile.NewPrecompile(
+     common.GovKeeper(app.GovKeeper),
+     appCodec,
+     addressCodec,
+ )

Slashing Precompile

- slashingPrecompile, err := slashingprecompile.NewPrecompile(
-     app.SlashingKeeper,
-     appCodec,
-     validatorAddressCodec,
-     consensusAddressCodec,
- )
+ slashingPrecompile, err := slashingprecompile.NewPrecompile(
+     common.SlashingKeeper(app.SlashingKeeper),
+     appCodec,
+     validatorAddressCodec,
+     consensusAddressCodec,
+ )

If You Have Custom Precompiles

Update your custom precompile constructors to use interfaces:
// Your custom precompile
- func NewMyPrecompile(
-     bankKeeper bankkeeper.Keeper,
-     stakingKeeper stakingkeeper.Keeper,
- ) (*MyPrecompile, error) {
+ func NewMyPrecompile(
+     bankKeeper common.BankKeeper,      // Use interface
+     stakingKeeper common.StakingKeeper, // Use interface
+ ) (*MyPrecompile, error) {
Testing benefit: You can now use mock keepers:
mockBank := &MockBankKeeper{}
precompile, _ := NewMyPrecompile(mockBank, mockStaking)

4) Mempool Changes

Configuration-Based Architecture

The Problem: In v0.4.0, you had to construct complete TxPool and CosmosPool instances with 20+ parameters each. This was error-prone and required deep understanding of Ethereum internals. The Solution: v0.5.0 accepts configuration objects with smart defaults. You only configure what differs from standard behavior.

Migration for Default Configurations

If you’re using standard mempool settings, the migration is minimal:
// v0.4.0 and v0.5.0 - Same for basic usage
mempoolConfig := &evmmempool.EVMMempoolConfig{
    AnteHandler:   app.GetAnteHandler(),
    BlockGasLimit: 0,  // 0 means use default 100M
}
What happens with defaults:
  • Transaction capacity: 10,000 EVM + 5,000 Cosmos transactions
  • Gas pricing: 1 gwei minimum
  • Nonce ordering: Strict per account
  • Replacement rules: 10% gas increase required

Migration for Custom Configurations

If you previously customized transaction pools:
mempoolConfig := &evmmempool.EVMMempoolConfig{
-   // v0.4.0: Had to build entire pool
-   TxPool: &txpool.TxPool{
-       config: txpool.Config{
-           Locals:       []common.Address{...},
-           NoLocals:     false,
-           Journal:      ".ethereum/transactions.rlp",
-           Rejournal:    time.Hour,
-           PriceLimit:   1,
-           PriceBump:    10,
-           AccountSlots: 16,
-           GlobalSlots:  10000,
-           // ... 15+ more fields
-       },
-   },
-   CosmosPool: customCosmosPool,

+   // v0.5.0: Just provide overrides
+   LegacyPoolConfig: &legacypool.Config{
+       AccountSlots: 64,      // Override: more txs per account
+       GlobalSlots:  50000,   // Override: bigger pool
+       // Defaults used for everything else
+   },
+   CosmosPoolConfig: nil,  // Use all defaults

    AnteHandler:   app.GetAnteHandler(),
    BlockGasLimit: 200_000_000,  // Custom: higher gas limit
}
Key configuration parameters:
ParameterDefaultWhen to Override
AccountSlots16High-frequency trading (set 64+)
GlobalSlots10,000High throughput chain (set 50,000+)
PriceLimit1 gweiPrivate chain (set 0)
BlockGasLimit100MGaming/DeFi chain (adjust as needed)

New MinTip Parameter

v0.5.0 adds MinTip for spam protection:
mempoolConfig := &evmmempool.EVMMempoolConfig{
    // ... other config
    MinTip: big.NewInt(1_000_000_000),  // 1 gwei minimum tip
}
Purpose: Prevents zero-fee spam transactions during high congestion. Default: 0 (accept any fee) Recommendation: Set to 1 gwei for public chains

5) Ante Handler Changes

Performance Optimizations

The ante handler system has been optimized to remove unnecessary EVM instance creation. What Changed:
  • CanTransfer ante decorator no longer creates StateDB instances
  • EVM instance removal improves performance for balance checks
  • Signature verification optimizations
Migration Impact:
  • Standard setups: No changes required
  • Custom ante handlers: Verify compatibility with new CanTransfer behavior
Custom Ante Handler Updates: If you have custom ante handlers that depend on EVM instance creation during balance checks:
/ Custom ante handler example
func (d MyCustomDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
    / Balance checking now optimized - no EVM instance creation
-   evm := d.evmKeeper.NewEVM(ctx, ...)
-   stateDB := evm.StateDB
-   balance := stateDB.GetBalance(address)
    
    / Use keeper method instead
+   balance := d.evmKeeper.GetBalance(ctx, address)
    
    return next(ctx, tx, simulate)
}

Field Mapping (v0.4.x → v0.5.0)

  • Removed fields:
    • TxPool (pre-built EVM pool)
    • CosmosPool (pre-built Cosmos pool)
  • New/Replacement fields:
    • LegacyPoolConfig (configure legacy EVM txpool behavior)
    • CosmosPoolConfig (configure Cosmos PriorityNonceMempool behavior)
    • BlockGasLimit (required; 0 uses fallback 100_000_000)
    • BroadcastTxFn (optional callback; defaults to broadcasting via clientCtx)
    • MinTip (optional minimum tip for EVM selection)

6) Global Mempool Removal

BREAKING: Singleton Pattern Eliminated

The Problem with v0.4.0: The global mempool singleton created several critical issues:
  • Testing nightmare: Couldn’t run parallel tests (global state conflicts)
  • Multi-chain impossible: Couldn’t run multiple chains in one process
  • Race conditions: Concurrent access to global state
  • Memory leaks: Global state never cleaned up
The v0.5.0 Solution: Direct dependency injection - each component receives its mempool reference explicitly.

Required Migration Steps

1. Remove Global Mempool Registration

// In app.go or app initialization
evmMempool := evmmempool.NewExperimentalEVMMempool(...)

- // DELETE THIS - no longer needed or supported
- if err := mempool.SetGlobalEVMMempool(evmMempool); err != nil {
-     panic(err)
- }
Impact if not removed: Compilation error - these functions no longer exist.

2. Update JSON-RPC Server Initialization

The JSON-RPC server now requires explicit mempool injection:
// In server/start.go or your server initialization
jsonRPCServer, err := jsonrpc.StartJSONRPC(
    ctx,
    clientCtx,
    logger.With("module", "jsonrpc"),
+   evmMempool,  // Pass mempool as parameter
    config,
    indexer,
)
Why this change: The JSON-RPC server needs mempool access for txpool_ methods. Previously it used the global, now it’s explicitly provided.

3. Update Custom Components

If you have custom components that accessed the global mempool:
// Custom component that used global mempool
type MyCustomService struct {
-   // No mempool field - used global
+   mempool *evmmempool.ExperimentalEVMMempool
}

- func NewMyCustomService() *MyCustomService {
+ func NewMyCustomService(mempool *evmmempool.ExperimentalEVMMempool) *MyCustomService {
    return &MyCustomService{
-       // Used to call mempool.GetGlobalEVMMempool() when needed
+       mempool: mempool,
    }
}

func (s *MyCustomService) DoSomething() {
-   pool := mempool.GetGlobalEVMMempool()
-   if pool == nil {
-       return errors.New("mempool not initialized")
-   }
+   // Use s.mempool directly
    pending := s.mempool.GetPendingTransactions()
}

Testing Benefits

The removal of global state enables proper testing:
// v0.5.0 - Can run parallel tests
func TestMempool(t *testing.T) {
    t.Parallel()  // Now safe!

    mempool1 := evmmempool.NewExperimentalEVMMempool(...)
    mempool2 := evmmempool.NewExperimentalEVMMempool(...)

    // Each test has isolated mempool
}

7) EIP-7702 EOA Code Delegation

NEW FEATURE: Account Abstraction for EOAs

EIP-7702 enables externally owned accounts to temporarily execute smart contract code through authorization lists. What’s New:
  • SetCodeTx Transaction Type: New transaction type supporting code delegation
  • Authorization Signatures: Signed permissions for code delegation
  • Temporary Execution: EOAs can execute contract logic for single transactions
  • Account Abstraction: Multi-sig, time-locks, automated strategies
Implementation: x/vm/keeper/state_transition.go:426+ Usage Example:
/ Enable EOA to execute as multicall contract
const authorization = await signAuthorization({
    chainId: 9000,
    address: multicallContractAddress,
    nonce: await wallet.getNonce(),
}, wallet);

const tx = {
    type: 4, / SetCodeTxType  
    authorizationList: [authorization],
    to: wallet.address,
    data: multicall.interface.encodeFunctionData("batchCall", [calls]),
    gasLimit: 500000,
};

await wallet.sendTransaction(tx);
Developer Impact:
  • Enhanced Wallets: EOAs can have programmable features
  • Better UX: Batched operations, custom validation logic
  • Account Abstraction: Multi-sig and advanced security features
  • No Migration: Existing EOAs enhanced without changes

8) EIP-2935 Block Hash Storage

NEW FEATURE: Historical Block Hash Access

EIP-2935 provides standardized access to historical block hashes via contract storage. What’s New:
  • BLOCKHASH opcode now queries contract storage for historical hashes
  • New history_serve_window parameter controls storage depth
  • Compatible with Ethereum’s EIP-2935 specification
Configuration:
/ Genesis parameter
"history_serve_window": 8192  / Default: 8192 blocks
Usage for Developers:
/ Smart contract can now reliably access historical block hashes
contract HistoryExample {
    function getRecentBlockHash(uint256 blockNumber) public view returns (bytes32) {
        / Works for blocks within history_serve_window range
        return blockhash(blockNumber);
    }
}
Performance Considerations:
  • Larger history_serve_window values increase storage requirements
  • Default of 8192 provides good balance of utility and performance
  • Values > 8192 may impact node performance

8) New RPC Methods

eth_createAccessList

New JSON-RPC method for creating access lists to optimize transaction costs. Usage:
curl -X POST \
  -H "Content-Type: application/json" \
  --data '{
    "jsonrpc": "2.0",
    "method": "eth_createAccessList",
    "params": [{
      "to": "0x...",
      "data": "0x...",
      "gas": "0x...",
      "gasPrice": "0x..."
    }, "latest"],
    "id": 1
  }' \
  http://localhost:8545
Response:
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "accessList": [
      {
        "address": "0x...",
        "storageKeys": ["0x..."]
      }
    ],
    "gasUsed": "0x..."
  }
}

9) Performance Improvements

Gas Estimation Optimization

  • Short-circuit plain transfers: Simple ETH transfers now bypass complex gas estimation
  • Optimistic bounds: Uses MaxUsedGas for better initial estimates
  • Result: Significantly faster eth_estimateGas performance

State Management

  • Reduced EVM instances: Fewer unnecessary EVM instance creations
  • Storage optimizations: Empty storage checks implemented per EIP standards
  • Block notifications: Improved timing prevents funding errors

Mempool Enhancements

  • Nonce gap handling: Better error handling for transaction sequencing
  • Configuration flexibility: Tunable parameters for different network conditions

10) Testing Your Migration

Pre-Upgrade Checklist

Pre-Upgrade Checklist
# 1. Backup current state
evmd export > pre-upgrade-state.json

# 2. Document existing parameters
evmd query vm params > pre-upgrade-params.json

# 3. Note active precompiles
evmd query erc20 token-pairs > pre-upgrade-token-pairs.json

Post-Upgrade Verification

Post-Upgrade Verification
# 1. Verify node starts successfully
evmd start

# 2. Test EVM functionality
cast send --rpc-url http://localhost:8545 --private-key $PRIVATE_KEY \
  0x... "transfer(address,uint256)" 0x... 1000

# 3. Verify new RPC methods
curl -X POST \
  -H "Content-Type: application/json" \
  --data '{"jsonrpc":"2.0","method":"eth_createAccessList","params":[...],"id":1}' \
  http://localhost:8545

# 4. Test precompile functionality
cast call 0x... "balanceOf(address)" 0x... --rpc-url http://localhost:8545

# 5. Verify EIP-2935 support
cast call --rpc-url http://localhost:8545 \
  $CONTRACT_ADDRESS "getBlockHash(uint256)" $BLOCK_NUMBER

Integration Tests

Integration Test Example
/ Example integration test
func TestV050Migration(t *testing.T) {
    / Test mempool configuration
    config := &evmmempool.EVMMempoolConfig{
        AnteHandler:   anteHandler,
        BlockGasLimit: 100_000_000,
    }
    mempool := evmmempool.NewExperimentalEVMMempool(...)
    require.NotNil(t, mempool)
    
    / Test precompile interfaces
    bankPrecompile, err := bankprecompile.NewPrecompile(
        common.BankKeeper(bankKeeper),
    )
    require.NoError(t, err)
    
    / Test parameter validation
    params := vmtypes.NewParams(...)
    require.Equal(t, uint64(8192), params.HistoryServeWindow)
    require.False(t, hasAllowUnprotectedTxs(params)) / Should be removed
}

11) Rollback Plan

If issues arise during migration:
# 1. Stop the upgraded node
systemctl stop evmd

# 2. Restore pre-upgrade binary
cp evmd-v0.4.1 /usr/local/bin/evmd

# 3. Restore pre-upgrade genesis (if needed)
cp pre-upgrade-genesis.json ~/.evmd/config/genesis.json

# 4. Restart with previous version
systemctl start evmd

12) Common Migration Issues

Issue: Precompile Constructor Errors

error: cannot use bankKeeper (type bankkeeper.Keeper) as type common.BankKeeper
Solution: Cast concrete keepers to interfaces:
bankPrecompile, err := bankprecompile.NewPrecompile(
    common.BankKeeper(bankKeeper), / Add interface cast
)

Issue: Genesis Validation Failure

error: unknown field 'allow_unprotected_txs' in vm params
Solution: Remove the parameter from genesis:
# Update genesis.json to remove allow_unprotected_txs
# Add history_serve_window with default value 8192

Issue: Mempool Initialization Panic

panic: config must not be nil
Solution: Always provide mempool configuration:
config := &evmmempool.EVMMempoolConfig{
    AnteHandler:   app.GetAnteHandler(),
    BlockGasLimit: 100_000_000,
}

Issue: Global Mempool Access

error: undefined: mempool.GetGlobalEVMMempool
Solution: Pass mempool directly instead of using global access:
/ Pass mempool as parameter
func NewRPCService(mempool *evmmempool.ExperimentalEVMMempool) {
    / Use injected mempool
}

13) Summary

v0.5.0 introduces significant improvements in performance, EVM compatibility, and code architecture:
  • EIP-2935 enables reliable historical block hash access
  • Precompile interfaces improve modularity and testing
  • Mempool optimizations provide better configurability
  • Performance improvements reduce gas estimation latency
  • New RPC methods enhance developer experience
Review all breaking changes carefully and test thoroughly before deploying to production. For additional support, refer to:
I