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
type EVMMempoolConfig struct {
// Required: AnteHandler for transaction validation
AnteHandler sdk . AnteHandler
// Required: Block gas limit for transaction selection
BlockGasLimit uint64
// Optional: Custom legacy pool configuration (replaces TxPool)
LegacyPoolConfig * legacypool . Config
// Optional: Custom Cosmos pool configuration (replaces CosmosPool)
CosmosPoolConfig * sdkmempool . PriorityNonceMempoolConfig [ math . Int ]
// Optional: Custom broadcast function for promoted transactions
BroadcastTxFn func ( txs [] * ethtypes . Transaction ) error
// Optional: Minimum tip required for EVM transactions
MinTip * uint256 . Int
}
See all 19 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