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

74891 bc insight txpool admission omits operator fee solvency

Submitted on Apr 25th 2026 at 17:36:09 UTC by @y4y for Audit Comp | Base Azul

  • Report ID: #74891

  • Report Type: Blockchain/DLT

  • Report severity: Insight

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

  • Impacts:

    • Causing network processing nodes to process transactions from the mempool beyond set parameters

Description

Brief/Intro

When Isthmus operator fees are active, txpool admission and execution disagree about the sender balance required for the same non-deposit transaction. The txpool path only checks:

tx.cost + l1_data_fee

while execution later charges:

tx.cost + l1_data_fee + operator_fee

Any public sender can therefore choose a balance inside that solvency gap and submit a transaction that is accepted into the mempool, propagated, and picked up by the builder, but deterministically rejected during execution as LackOfFundForMaxFee.

In Base's flashblocks path this is not a one-shot rejection. The invalid transaction is only marked invalid in the current transaction iterator, while the next flashblock refreshes a new iterator from the same pool contents. The same bad transaction can therefore be reconsidered and rejected repeatedly.

Vulnerability Details

The txpool validator only adds the L1 data fee to the ordinary EIP-1559 max cost before deciding whether the sender can afford the transaction:

The helper used there excludes the operator fee and returns only the L1 data component:

Execution uses a stricter cost function. tx_cost_with_tx() delegates to tx_cost(), which adds the operator fee whenever Isthmus is active:

That stricter total is deducted before ordinary execution proceeds:

Concrete example from the verified local PoC:

  • attack tx gas_limit = 400000

  • attack tx max_fee_per_gas = 3003000000

  • l1_data_fee = 122

  • operator_fee = 1000000000000000

So:

This satisfies:

so the transaction is admitted by txpool but cannot pass execution solvency.

The repeated resource drain happens because flashblocks refreshes a new best-tx iterator on each flashblock:

When execution rejects the transaction, the builder only marks it invalid in the current iterator:

That invalidation is local to the active iterator:

So the next flashblock reconstructs the iterator from the pool and sees the same bad transaction again.

The pre-condition for this to happen is:

  • Isthmus or later rules are active, so operator fees are charged.

  • The sender submits a normal non-deposit transaction whose balance satisfies: tx.cost + l1_data_fee <= balance < tx.cost + l1_data_fee + operator_fee.

  • The transaction reaches the public txpool and builder path.

  • For meaningful amplification, the attacker uses many EOAs or repeats the pattern across many nonces/accounts.

This does not require malformed input, a compromised signer, a trusted-role mistake, or any non-default deployment assumption beyond public transaction submission.

Impact Details

  1. invalid-but-admitted transactions enter and persist in the mempool;

  2. builders repeatedly pull them into flashblock selection;

  3. execution rejects them as underfunded only after builder work has already been spent;

  4. the transactions remain Known and visible in txpool_content, so the work repeats.

That gives an attacker a repeatable CPU / builder-throughput degradation primitive plus pending-pool pollution. On the verified local runs:

  • 128 attacker EOAs stayed Known, stayed in txpool_content, and produced 76 repeated 128 considered / 0 included / 128 rejected flashblock cycles with peak builder CPU 55.59%.

  • 512 attacker EOAs stayed Known, stayed in txpool_content, and pushed the same builder to peak CPU 88.47%.

The resource effect scaled much more clearly in CPU than RAM on local hardware, which is consistent with a repeated selection-and-reject workload rather than a simple memory leak.

References

  • base/crates/execution/txpool/src/validator.rs:181-255

  • base/crates/common/evm/src/l1block.rs:259-281

  • base/crates/common/evm/src/handler.rs:154-174

  • base/crates/builder/core/src/flashblocks/payload.rs:572-575

  • base/crates/builder/core/src/flashblocks/context.rs:821-845

  • reth/crates/transaction-pool/src/pool/best.rs:110-117

Proof of Concept

Logging-only txpool admission proof

File:

  • crates/execution/txpool/src/validator.rs

Why this change exists:

  • the underlying bug already existed;

  • this patch only emits a warning when a transaction passes txpool solvency but would fail executor solvency after operator fee is added;

  • it does not alter txpool admission behavior.

Exact diff:

Devnet operator-fee plumbing

These files exist only so the local devnet can reproduce the Isthmus operator fee path with a non-zero constant.

devnet/src/setup/container.rs

etc/docker/devnet-env

etc/scripts/devnet/setup-l2.sh

etc/scripts/devnet/templates/l2-intent.toml.template

Builder log visibility

File:

  • etc/docker/docker-compose.yml

Why this change exists:

  • the live flashblocks path already logs invalid transactions at trace;

  • without this filter, the builder still shows rejection_reasons=["other"], but the explicit lack of funds reason is hidden.

Exact diff:

The E2E PoC driver

Files:

  • devnet/examples/h06_operator_fee_gap.rs

  • devnet/Cargo.toml

  • Cargo.lock

Why these changes exist:

  • the example is the actual E2E reproducer;

  • it computes live l1_data_fee and operator_fee from on-chain state;

  • it funds accounts into the exact solvency gap;

  • it sends the attack transactions;

  • it verifies that they remain Known, remain in txpool_content, and never get mined during the observation window;

  • it samples docker stats so the run records resource impact;

  • it cleans up the attack senders afterward.

Dependency wiring:

The actual PoC, put under base/devnet/examples/operator_fee_gap.rs:

Attack Cost and Recoverability The attack is primarily capital-backed, not gas-burning.

The bad attack transactions themselves are not mined, so they do not burn their advertised gas, L1 data fee, or operator fee. In the verified PoC:

  • attack_txs_mined = 0

  • txpool_hash_hits = attack_txs_known

That means the principal seeded into attacker EOAs remains attacker-owned.

The economics break down into three parts:

  1. Temporary locked capital The attacker must lock enough balance in each EOA to satisfy txpool solvency. That principal is recoverable in principle because the attack txs are never included.

  2. Unrecoverable setup / teardown fees Any included funding, top-up, cancel, or sweep transaction still pays normal on-chain fees. Those fees are not recoverable.

  3. Opportunity cost / operational friction While the bad tx remains pending, the account's nonce is occupied. Immediate recovery therefore requires a same-nonce replacement transaction, not a cheaper later nonce.

Txpool explicitly supports same-nonce replacement when the replacement is not underpriced:

And the default replacement threshold for regular transactions is 10%:

So the attacker has two realistic recovery paths:

  • Passive recovery: wait for the bad tx to leave the pool naturally (restart, eviction, or operator action), then reuse the principal. In this path the per-round unrecoverable cost can be near zero if the attacker already controls pre-funded EOAs.

  • Active recovery: immediately replace each pending nonce-0 attack tx with a higher-priced same-nonce transaction and sweep funds back. In this path the recovery tx fees are unrecoverable.

Local PoC economics for the 128-sender run, using the intentionally large devnet operator_fee_constant = 1000000000000000:

  • seeded principal across attacker EOAs: 185753600000019200 wei (0.1857536000000192 ETH)

  • attack-tx fee burn: 0 wei because none of the attack txs were mined

  • approximate unrecoverable funding fees for 128 fresh EOAs: 136072064000000000 wei (0.136072064 ETH)

    • this is 128 * (21000 * 3003000000 + operator_fee_constant), ignoring the tiny L1-data term

  • approximate unrecoverable immediate-recovery fees with the default 10% replacement bump: 136879270400000000 wei (0.1368792704 ETH)

  • approximate principal recoverable by immediate sweep after paying those recovery fees: 48874329600019200 wei (0.0488743296000192 ETH)

Tha main impact of this issue is still "causing network processing nodes to process mempool transactions beyond intended solvency parameters", but it can be used in an adversarial way to exhaust computational resources.

To run the PoC:

1

Start the single-sequencer devnet with a non-zero operator fee:

2

Wait until just devnet status shows the stack healthy.

3

Run the main E2E PoC:

4

Optional stronger scaling run:

The PoC driver:

  • reads the live fee scalars from L1BlockInfo;

  • computes validation_total and execution_total for each signed raw tx;

  • funds each random sender into the solvency gap;

  • submits all attack txs to the builder RPC;

  • polls:

    • base_transactionStatus

    • eth_getTransactionReceipt

    • txpool_content

    • docker stats base-builder

  • drops the attacker senders from the txpool at the end.

The following contains output in local test: Representative output from the verified 128-sender run:

Per-block samples from that same run:

Txpool-side proof:

Flashblocks-side execution proof:

Flashblock summary showing repeated full rejection with zero inclusion:

That exact 128 considered / 0 included / 128 rejected pattern appeared 76 times during the reproduced run.

Representative output from the verified 512-sender run:

The strongest local CPU samples from that run were:

Was this helpful?