This migration contains breaking changes that require code updates v0.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
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:
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
}
}
}
}
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 :
Parameter Default When to Override AccountSlots 16 High-frequency trading (set 64+) GlobalSlots 10,000 High throughput chain (set 50,000+) PriceLimit 1 gwei Private chain (set 0) BlockGasLimit 100M Gaming/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
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..."
}
}
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
# 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
See all 8 lines
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
See all 19 lines
Integration Tests
/ 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
}
See all 21 lines
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: