For the complete documentation index, see llms.txt. This page is also available as Markdown.

74725 bc insight post isthmus txpool admission omits operator fee affordability causing repeated processing of unexecutable mempool transactions

Submitted on Apr 24th 2026 at 14:32:15 UTC by @OxPrince for Audit Comp | Base Azul

  • Report ID: #74725

  • Report Type: Blockchain/DLT

  • Report severity: Insight

  • Target: https://github.com/base/base/tree/v0.8.0-rc.24

  • Impacts:

    • Increasing network processing node resource consumption by at least 30% without brute force actions, compared to the preceding 24 hours

Description

Brief/Intro

After Isthmus is active, Base execution charges each non-deposit transaction an additional operator fee. The txpool admission check does not include this operator fee when deciding whether the sender can afford the transaction. As a result, an ordinary sender can submit transactions that pass txpool validation but are underfunded for real execution. These transactions remain pending and are repeatedly returned by fresh txpool scans, causing network processing nodes to spend repeated work on transactions that should have been rejected before entering the pool.

Vulnerability Details

The bug is a mismatch between the txpool-side affordability check and the execution-side affordability check.

In crates/execution/txpool/src/validator.rs, OpTransactionValidator::apply_op_checks adds only the L1 data fee to the normal transaction cost:

let cost_addition = match l1_block_info.l1_tx_data_fee(
    self.chain_spec(),
    self.block_timestamp(),
    &encoded,
    false,
) {
    Ok(cost) => cost,
    Err(err) => {
        return TransactionValidationOutcome::Error(*valid_tx.hash(), Box::new(err));
    }
};
let cost = valid_tx.transaction().cost().saturating_add(cost_addition);

This means txpool admission checks:

However, the execution path charges the full additional Optimism/Base cost. In crates/common/evm/src/handler.rs, execution calls chain.tx_cost_with_tx(tx, spec) and subtracts that amount from the sender balance before executing the transaction. In crates/common/evm/src/l1block.rs, tx_cost_with_tx delegates to tx_cost, which adds the operator fee when Isthmus is enabled:

So execution requires:

This creates an exploitable affordability window. A sender can fund an account with exactly enough balance for tx.cost + l1_tx_data_fee, but not enough for tx.cost + l1_tx_data_fee + operator_fee. The txpool accepts the transaction because it uses the smaller cost. Execution later rejects it because the full post-Isthmus cost cannot be paid.

This is not a user mistake or a configuration issue. The sender uses an ordinary externally submitted EIP-1559 transaction. The invalid admission happens because txpool validation omits a fee component that execution correctly enforces after Isthmus.

The repeated-work path is also present in the normal node flow:

  • the txpool consumer creates a fresh best_transactions() iterator on every loop;

  • the payload builder creates a fresh best-transaction iterator for each payload build attempt;

  • invalid payload-builder candidates are marked invalid only for the current iterator pass, so that does not remove the underlying transaction from the txpool.

Impact Details

The practical impact is wasted network node work:

  • transactions that should fail admission enter the pending pool;

  • txpool scans keep resurfacing those transactions;

  • payload-building paths can repeatedly consider and attempt invalid candidates;

  • each rejected candidate consumes node CPU and processing time before being skipped for that iterator pass.

This does not require privileged access, sequencer control, admin mistakes, or invalid protocol configuration. An attacker only needs to create ordinary transactions from accounts funded into the mismatch window:

The attack does not cause theft and does not make invalid transactions successfully execute. Execution correctly rejects the transactions. The vulnerability is that txpool admission under-meters post-Isthmus affordability and therefore admits transactions that downstream network processing should never have needed to handle.

References

  • crates/execution/txpool/src/validator.rs:202 - txpool adds only l1_tx_data_fee(...).

  • crates/execution/txpool/src/validator.rs:213 - txpool affordability check uses tx.cost + l1_tx_data_fee.

  • crates/common/evm/src/handler.rs:157 - execution performs the additional-cost balance deduction.

  • crates/common/evm/src/l1block.rs:262 - tx_cost_with_tx accounts for L1 fee and operator fee.

  • crates/common/evm/src/l1block.rs:271 - tx_cost computes the additional transaction cost.

  • crates/common/evm/src/l1block.rs:276 - operator fee is added when Isthmus is enabled.

  • crates/execution/txpool/src/transaction.rs:548 - root-cause PoC proving txpool accepts an operator-fee-underfunded transaction.

  • crates/execution/txpool/src/transaction.rs:638 - amplification PoC proving repeated scans produce 100% extra processing over the one-pass baseline.

  • crates/execution/txpool/src/consumer/task.rs:53 - consumer creates a fresh best_transactions() iterator each loop.

  • crates/execution/payload/src/builder.rs:259 - payload building obtains fresh best transactions from the pool.

  • crates/execution/payload/src/builder.rs:360 - payload builder consumes mempool transactions when txpool use is enabled.

  • crates/execution/payload/src/builder.rs:745 - invalid payload candidates are marked invalid for the current iterator pass.

Proof of Concept

The first test proves the root admission mismatch:

It creates a post-Isthmus transaction, computes both:

Then it funds the sender with:

The test asserts that the full execution-side cost is higher, then confirms that txpool still returns TransactionValidationOutcome::Valid.

The second test is the main impact PoC:

It performs the following steps:

  1. Configures the txpool validator with post-Isthmus L1 info containing a non-zero operator fee.

  2. Creates 12 ordinary signed EIP-1559 transactions from separate senders.

  3. Funds each sender so the account covers tx.cost + l1_tx_data_fee, but not tx.cost + l1_tx_data_fee + operator_fee.

  4. Inserts all 12 transactions into a real Pool<OpTransactionValidator, BaseOrdering, InMemoryBlobStore>.

  5. Takes two fresh best_transactions() scans from the pool.

  6. Verifies that both scans return the same 12 execution-underfunded transaction hashes.

  7. Calculates the repeated-processing increase:

Was this helpful?