Overview
This guide provides step-by-step instructions for integrating the EVM mempool into your Cosmos EVM chain. For conceptual information about mempool design and architecture, see the Mempool Concepts page.
Step 1: Add EVM Mempool to App Struct
// Update your app/app.go to include the EVM mempool
type App struct {
* baseapp . BaseApp
// ... other keepers
// Cosmos EVM keepers
FeeMarketKeeper feemarketkeeper . Keeper
EVMKeeper * evmkeeper . Keeper
EVMMempool * evmmempool . ExperimentalEVMMempool
}
See all 10 lines
The mempool must be initialized after the antehandler has been set in the app.
Add the following configuration in your NewApp constructor:
// Set the EVM priority nonce mempool
if evmtypes . GetChainConfig () != nil {
mempoolConfig := & evmmempool . EVMMempoolConfig {
AnteHandler : app . GetAnteHandler (),
BlockGasLimit : 100_000_000 , // or 0 to use default
}
evmMempool := evmmempool . NewExperimentalEVMMempool (
app . CreateQueryContext ,
logger ,
app . EVMKeeper ,
app . FeeMarketKeeper ,
app . txConfig ,
app . clientCtx ,
mempoolConfig ,
)
app . EVMMempool = evmMempool
// Replace BaseApp mempool
app . SetMempool ( evmMempool )
// Set custom CheckTx handler for nonce gap support
checkTxHandler := evmmempool . NewCheckTxHandler ( evmMempool )
app . SetCheckTxHandler ( checkTxHandler )
// Set custom PrepareProposal handler
abciProposalHandler := baseapp . NewDefaultProposalHandler ( evmMempool , app )
abciProposalHandler . SetSignerExtractionAdapter (
evmmempool . NewEthSignerExtractionAdapter (
sdkmempool . NewDefaultSignerExtractionAdapter (),
),
)
app . SetPrepareProposal ( abciProposalHandler . PrepareProposalHandler ())
}
See all 34 lines
Breaking Change from v0.4.x: The global mempool registry (SetGlobalEVMMempool) has been removed. Mempool is now passed directly to the JSON-RPC server during initialization.
Configuration Options
The EVMMempoolConfig struct provides several configuration options for customizing the mempool behavior:
For most use cases, the minimal configuration is sufficient: mempoolConfig := & evmmempool . EVMMempoolConfig {
AnteHandler : app . GetAnteHandler (),
BlockGasLimit : 100_000_000 , // or 0 to use default
}
See all 4 lines
Defaults and Fallbacks
If BlockGasLimit is 0, the mempool uses a fallback of 100_000_000 gas.
If LegacyPoolConfig is not provided, defaults from legacypool.DefaultConfig are used.
If CosmosPoolConfig is not provided, a default PriorityNonceMempool is created with:
Priority = (fee_amount / gas_limit) in the chain bond denom
Comparator = big-int comparison (higher is selected first)
MinValue = 0
If BroadcastTxFn is not provided, a default is created that uses the app clientCtx/txConfig to broadcast EVM transactions when they are promoted from queued → pending.
MinTip is optional. If unset, selection uses the effective tip from each tx (min(gas_tip_cap, gas_fee_cap - base_fee)).
v0.4.x to v0.5.0 Migration
Breaking Change: Pre-built pools replaced with configuration objects
PR #496 replaced pre-built pools with configs in EVMMempoolConfig:
Removed: TxPool *txpool.TxPool, CosmosPool sdkmempool.ExtMempool
Added: LegacyPoolConfig *legacypool.Config, CosmosPoolConfig *sdkmempool.PriorityNonceMempoolConfig[math.Int]
Minimal Setups: Nothing to Change
If you use the default mempool wiring (no custom pools), your existing code continues to work:
mempoolConfig := & evmmempool . EVMMempoolConfig {
AnteHandler : app . GetAnteHandler (),
BlockGasLimit : 100_000_000 , // or 0 to use default
}
evmMempool := evmmempool . NewExperimentalEVMMempool (
app . CreateQueryContext , logger , app . EVMKeeper , app . FeeMarketKeeper ,
app . txConfig , app . clientCtx , mempoolConfig
)
See all 8 lines
Advanced Setups: Migrate Your Customizations
If you built custom pools yourself, replace them with configuration objects:
Before (v0.4.x):
mempoolConfig := & evmmempool . EVMMempoolConfig {
TxPool : customTxPool , // ← REMOVED
CosmosPool : customCosmosPool , // ← REMOVED
AnteHandler : app . GetAnteHandler (),
BlockGasLimit : 100_000_000 ,
}
See all 6 lines
After (v0.5.0):
mempoolConfig := & evmmempool . EVMMempoolConfig {
LegacyPoolConfig : & legacyCfg , // ← NEW (or nil for defaults)
CosmosPoolConfig : & cosmosCfg , // ← NEW (or nil for defaults)
AnteHandler : app . GetAnteHandler (),
BlockGasLimit : 100_000_000 ,
}
See all 6 lines
Custom Legacy Pool Configuration
Customize EVM transaction pool parameters:
// EVM legacy txpool tuning
legacyCfg := legacypool . DefaultConfig
legacyCfg . PriceLimit = 2 // Minimum gas price (wei)
legacyCfg . PriceBump = 15 // 15% price bump to replace
legacyCfg . AccountSlots = 32 // Slots per account
legacyCfg . GlobalSlots = 10240 // Total executable slots
legacyCfg . AccountQueue = 128 // Non-executable per account
legacyCfg . GlobalQueue = 2048 // Total non-executable
legacyCfg . Lifetime = 6 * time . Hour // Max queue time
mempoolConfig . LegacyPoolConfig = & legacyCfg
See all 11 lines
Custom Cosmos Mempool Configuration
The mempool uses a PriorityNonceMempool for Cosmos transactions by default. You can customize the priority calculation:
// Define custom priority calculation for Cosmos transactions
cosmosCfg := sdkmempool . PriorityNonceMempoolConfig [ math . Int ]{}
cosmosCfg . TxPriority = sdkmempool . TxPriority [ math . Int ]{
GetTxPriority : func ( goCtx context . Context , tx sdk . Tx ) math . Int {
feeTx , ok := tx .( sdk . FeeTx )
if ! ok {
return math . ZeroInt ()
}
// Get fee in bond denomination
bondDenom := "uatom" // or your chain's bond denom
fee := feeTx . GetFee ()
found , coin := fee . Find ( bondDenom )
if ! found {
return math . ZeroInt ()
}
// Calculate gas price: fee_amount / gas_limit
gasPrice := coin . Amount . Quo ( math . NewIntFromUint64 ( feeTx . GetGas ()))
return gasPrice
},
Compare : func ( a , b math . Int ) int {
return a . BigInt (). Cmp ( b . BigInt ()) // Higher values have priority
},
MinValue : math . ZeroInt (),
}
mempoolConfig . CosmosPoolConfig = & cosmosCfg
See all 28 lines
Custom Broadcast Function
Override the default broadcast behavior for promoted EVM transactions:
// Custom EVM broadcast (optional)
mempoolConfig . BroadcastTxFn = func ( txs [] * ethtypes . Transaction ) error {
// Custom logic for broadcasting promoted transactions
return nil
}
See all 5 lines
Custom Block Gas Limit
Different chains may require different gas limits based on their capacity:
// Example: 50M gas limit for lower capacity chains
mempoolConfig := & evmmempool . EVMMempoolConfig {
AnteHandler : app . GetAnteHandler (),
BlockGasLimit : 50_000_000 ,
}
See all 5 lines
Event Bus Integration
For best results, connect the mempool to CometBFT’s EventBus so it can react to finalized blocks:
// After starting the CometBFT node
if m , ok := app . GetMempool ().( * evmmempool . ExperimentalEVMMempool ); ok {
m . SetEventBus ( bftNode . EventBus ())
}
See all 4 lines
This enables chain-head notifications so the mempool can promptly promote/evict transactions when blocks are committed.
Architecture Components
The EVM mempool consists of several key components:
ExperimentalEVMMempool
The main coordinator implementing Cosmos SDK’s ExtMempool interface (mempool/mempool.go).
Key Methods :
Insert(ctx, tx): Routes transactions to appropriate pools
Select(ctx, filter): Returns unified iterator over all transactions
Remove(tx): Handles transaction removal with EVM-specific logic
InsertInvalidNonce(txBytes): Queues nonce-gapped EVM transactions locally
CheckTx Handler
Custom transaction validation that handles nonce gaps specially (mempool/check_tx.go).
Special Handling : On ErrNonceGap for EVM transactions:
if errors . Is ( err , ErrNonceGap ) {
// Route to local queue instead of rejecting
err := mempool . InsertInvalidNonce ( request . Tx )
// Must intercept error and return success to EVM client
return interceptedSuccess
}
See all 6 lines
TxPool
Direct port of Ethereum’s transaction pool managing both pending and queued transactions (mempool/txpool/).
Key Features :
Uses vm.StateDB interface for Cosmos state compatibility
Implements BroadcastTxFn callback for transaction promotion
Cosmos-specific reset logic for instant finality
PriorityNonceMempool
Standard Cosmos SDK mempool for non-EVM transactions with fee-based prioritization.
Default Priority Calculation :
// Calculate effective gas price
priority = ( fee_amount / gas_limit ) - base_fee
See all 2 lines
Transaction Type Routing
The mempool handles different transaction types appropriately:
Ethereum Transactions (MsgEthereumTx)
Tier 1 (Local) : EVM TxPool handles nonce gaps and promotion
Tier 2 (Network) : CometBFT broadcasts executable transactions
Cosmos Transactions (Bank, Staking, Gov, etc.)
Direct to Tier 2 : Always go directly to CometBFT mempool
Standard Flow : Follow normal Cosmos SDK validation and broadcasting
Priority-Based : Use PriorityNonceMempool for fee-based ordering
Unified Transaction Selection
During block building, both transaction types compete fairly based on their effective tips:
// Simplified selection logic
func SelectTransactions () Iterator {
evmTxs := GetPendingEVMTransactions () // From local TxPool
cosmosTxs := GetPendingCosmosTransactions () // From Cosmos mempool
return NewUnifiedIterator ( evmTxs , cosmosTxs ) // Fee-based priority
}
See all 7 lines
Fee Comparison :
EVM : gas_tip_cap or min(gas_tip_cap, gas_fee_cap - base_fee)
Cosmos : (fee_amount / gas_limit) - base_fee
Selection : Higher effective tip gets selected first
Testing Your Integration
Verify Nonce Gap Handling
Test that transactions with nonce gaps are properly queued:
// Send transactions out of order
await wallet . sendTransaction ({ nonce: 100 , ... }); // OK: Immediate execution
await wallet . sendTransaction ({ nonce: 102 , ... }); // OK: Queued locally (gap)
await wallet . sendTransaction ({ nonce: 101 , ... }); // OK: Fills gap, both execute
See all 4 lines
Test Transaction Replacement
Verify that higher-fee transactions replace lower-fee ones:
// Send initial transaction
const tx1 = await wallet . sendTransaction ({
nonce: 100 ,
gasPrice: parseUnits ( "20" , "gwei" )
});
// Replace with higher fee
const tx2 = await wallet . sendTransaction ({
nonce: 100 , // Same nonce
gasPrice: parseUnits ( "30" , "gwei" ) // Higher fee
});
// tx1 is replaced by tx2
See all 12 lines
Verify Batch Deployments
Test typical deployment scripts (like Uniswap) that send many transactions at once:
// Deploy multiple contracts in quick succession
const factory = await Factory . deploy ();
const router = await Router . deploy ( factory . address );
const multicall = await Multicall . deploy ();
// All transactions should queue and execute properly
See all 5 lines
Monitoring and Debugging
Use the txpool RPC methods to monitor mempool state:
txpool_status: Get pending and queued transaction counts
txpool_content: View all transactions in the pool
txpool_inspect: Get human-readable transaction summaries
txpool_contentFrom: View transactions from specific addresses