This mempool is experimental and in active development. It is intended for testing and evaluation purposes. Use in production environments is not recommended without thorough testing and risk assessment.
Please report issues and submit feedback to help improve stability. Overview
This guide explains how to integrate the EVM mempool in your Cosmos SDK chain to enable Ethereum-compatible transaction flows, including out-of-order transactions and nonce gap handling.
Prerequisites
Before integrating the EVM mempool:
- EVM Module Integration: Complete the EVM module integration first
- FeeMarket Module: Ensure the feemarket module is properly configured for base fee calculations
- Compatible AnteHandler: Your ante handler must support EVM transaction validation
Quick Start
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
}
The mempool must be initialized after the antehandler has been set in the app.
NewApp constructor:
// Set the EVM priority nonce mempool
if evmtypes.GetChainConfig() != nil {
    mempoolConfig := &evmmempool.EVMMempoolConfig{
        AnteHandler:   app.GetAnteHandler(),
        BlockGasLimit: 100_000_000,
    }
    evmMempool := evmmempool.NewExperimentalEVMMempool(
        app.CreateQueryContext,
        logger,
        app.EVMKeeper,
        app.FeeMarketKeeper,
        app.txConfig,
        app.clientCtx,
        mempoolConfig,
    )
    app.EVMMempool = evmMempool
    // Set the global mempool for RPC access
    if err := evmmempool.SetGlobalEVMMempool(evmMempool); err != nil {
        panic(err)
    }
    // 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())
}
Configuration Options
The EVMMempoolConfig struct provides several configuration options for customizing the mempool behavior:
Minimal Configuration
mempoolConfig := &evmmempool.EVMMempoolConfig{
    AnteHandler:   app.GetAnteHandler(),
    BlockGasLimit: 100_000_000, // 100M gas limit
}
Full Configuration Options
type EVMMempoolConfig struct {
    // Required: AnteHandler for transaction validation
    AnteHandler   sdk.AnteHandler
    // Required: Block gas limit for transaction selection
    BlockGasLimit uint64
    // Optional: Custom TxPool (defaults to LegacyPool)
    TxPool        *txpool.TxPool
    // Optional: Custom Cosmos pool (defaults to PriorityNonceMempool)
    CosmosPool    sdkmempool.ExtMempool
    // Optional: Custom broadcast function for promoted transactions
    BroadCastTxFn func(txs []*ethtypes.Transaction) error
}
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
priorityConfig := sdkmempool.PriorityNonceMempoolConfig[math.Int]{
    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 := "test" // 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 := &evmmempool.EVMMempoolConfig{
    AnteHandler:   app.GetAnteHandler(),
    BlockGasLimit: 100_000_000,
    CosmosPool:    sdkmempool.NewPriorityMempool(priorityConfig),
}
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,
}
Advanced Pool Parameter Customization
Modifying Hard-Coded Pool Limits
Several pool parameters are compiled into the source code and require modification for custom configurations:
TxPool Configuration Parameters
File: mempool/txpool/legacypool/legacypool.go
Default Configuration (lines ~45-55):
var DefaultConfig = Config{
    Locals:    []common.Address{},
    NoLocals:  false,
    Journal:   "",
    Rejournal: time.Hour,
    PriceLimit: 1,            // 1 gwei minimum gas price
    PriceBump:  10,           // 10% minimum price bump
    AccountSlots: 16,         // 16 pending transactions per account
    GlobalSlots:  4096,       // 4096 total pending transactions
    AccountQueue: 64,         // 64 queued transactions per account  
    GlobalQueue:  1024,       // 1024 total queued transactions
    Lifetime: 3 * time.Hour,  // 3 hour maximum queue time
}
// Create custom configuration in your app initialization
customConfig := legacypool.Config{
    Locals:    []common.Address{},
    NoLocals:  false,
    Journal:   "",
    Rejournal: time.Hour,
    PriceLimit: 5,            // 5 gwei minimum (higher than default)
    PriceBump:  15,           // 15% price bump (more aggressive)
    AccountSlots: 32,         // 32 pending per account (double default)
    GlobalSlots:  8192,       // 8192 total pending (double default)
    AccountQueue: 128,        // 128 queued per account (double default)
    GlobalQueue:  2048,       // 2048 total queued (double default)
    Lifetime: 6 * time.Hour,  // 6 hour queue time (double default)
}
// Use in mempool initialization
customTxPool := legacypool.New(customConfig, blockChain, opts...)
mempoolConfig := &evmmempool.EVMMempoolConfig{
    AnteHandler:   app.GetAnteHandler(),
    BlockGasLimit: 100_000_000,
    TxPool:        customTxPool,
}
High-Throughput Configuration
For chains handling high transaction volumes:
// File modification needed in mempool/txpool/legacypool/legacypool.go
highThroughputConfig := legacypool.Config{
    PriceLimit: 0,            // Accept zero gas price transactions
    PriceBump:  5,            // Lower bump requirement for faster replacement
    AccountSlots: 64,         // 4x more pending per account
    GlobalSlots:  16384,      // 4x more total pending
    AccountQueue: 256,        // 4x more queued per account
    GlobalQueue:  4096,       // 4x more total queued
    Lifetime: 12 * time.Hour, // Longer queue retention
}
Memory-Constrained Configuration
For resource-limited environments:
// Conservative memory usage configuration
conservativeConfig := legacypool.Config{
    PriceLimit: 10,           // Higher minimum to reduce spam
    AccountSlots: 8,          // Half the default pending slots
    GlobalSlots:  2048,       // Half the default total pending
    AccountQueue: 32,         // Half the default queued slots
    GlobalQueue:  512,        // Half the default total queued
    Lifetime: time.Hour,      // Shorter retention time
}
Custom TxPool Implementation
For complete control over pool behavior, implement a custom TxPool:
File: Create mempool/custom_pool.go
package mempool
import (
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core/types"
    legacypool "github.com/ethereum/go-ethereum/core/txpool/legacypool"
)
type CustomTxPool struct {
    *legacypool.LegacyPool
    customConfig Config
}
type Config struct {
    // Your custom configuration parameters
    MaxTxsPerAccount     int
    MaxGlobalTxs         int
    MinGasPriceGwei      int64
    ReplacementThreshold int
}
func NewCustomPool(config Config, blockchain *core.BlockChain) *CustomTxPool {
    legacyConfig := legacypool.Config{
        PriceLimit:   config.MinGasPriceGwei,
        PriceBump:    config.ReplacementThreshold,
        AccountSlots: uint64(config.MaxTxsPerAccount),
        GlobalSlots:  uint64(config.MaxGlobalTxs),
        // ... other parameters
    }
    
    pool := legacypool.New(legacyConfig, blockchain)
    return &CustomTxPool{
        LegacyPool:   pool,
        customConfig: config,
    }
}
// Add custom methods for advanced pool management
func (p *CustomTxPool) SetDynamicPricing(enabled bool) {
    // Implement dynamic gas pricing logic
}
func (p *CustomTxPool) GetPoolStatistics() PoolStats {
    // Return detailed pool statistics
    return PoolStats{
        PendingCount: p.Stats().Pending,
        QueuedCount:  p.Stats().Queued,
        // ... additional metrics
    }
}
// In your NewApp constructor
customPoolConfig := mempool.Config{
    MaxTxsPerAccount:     50,
    MaxGlobalTxs:        10000,
    MinGasPriceGwei:     2,
    ReplacementThreshold: 12,
}
customPool := mempool.NewCustomPool(customPoolConfig, blockChain)
mempoolConfig := &evmmempool.EVMMempoolConfig{
    AnteHandler:   app.GetAnteHandler(),
    BlockGasLimit: 200_000_000,
    TxPool:        customPool,
}
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
}
TxPool
Direct port of Ethereum’s transaction pool managing both pending and queued transactions (mempool/txpool/).
Key Features:
- Uses vm.StateDBinterface for Cosmos state compatibility
- Implements BroadcastTxFncallback 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
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
}
- EVM: gas_tip_capormin(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
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
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
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