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
}

Step 2: Configure Mempool in NewApp Constructor

The mempool must be initialized after the antehandler has been set in the app.
Add the following configuration in your NewApp constructor:
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:

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 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
}

v0.4.x to v0.5.0 Migration

Breaking Change: Pre-built pools replaced with configuration objects Before (v0.4.x):
mempoolConfig := &evmmempool.EVMMempoolConfig{
    TxPool:     customTxPool,      // ← REMOVED
    CosmosPool: customCosmosPool,  // ← REMOVED
    AnteHandler: app.GetAnteHandler(),
    BlockGasLimit: 100_000_000,
}
After (v0.5.0):
mempoolConfig := &evmmempool.EVMMempoolConfig{
    LegacyPoolConfig: &legacypool.Config{     // ← NEW
        AccountSlots: 16,
        GlobalSlots:  5120,
        PriceLimit:   1,
        // ... other config options
    },
    CosmosPoolConfig: &sdkmempool.PriorityNonceMempoolConfig[math.Int]{ // ← NEW
        TxPriority: customPriorityConfig,
    },
    AnteHandler:   app.GetAnteHandler(),
    BlockGasLimit: 100_000_000,
}

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,
    CosmosPoolConfig: &priorityConfig, // Pass config instead of pre-built pool
}

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,
}

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.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

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
}
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

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