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

74823 bc medium min base fee consolidation bypass below floor tx inclusion proof divergence

Submitted on Apr 25th 2026 at 04:14:49 UTC by @nord0x for Audit Comp | Base Azul

  • Report ID: #74823

  • Report Type: Blockchain/DLT

  • Report severity: Medium

  • 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

Bug Description

The Jovian consolidation check in attributes.rs:214 silently discards min_base_fee when comparing a sequencer-produced block against L1-derived attributes. The _ wildcard drops the field, so it's never compared. Two consequences:

  1. The base fee floor set by L1 SystemConfig governance is removed from subsequent block execution. The public RPC txpool then accepts and the block builder includes user transactions whose max_fee_per_gas is below the intended floor. This maps to the program's High impact: "Causing network processing nodes to process transactions from the mempool beyond set parameters."

  2. The proof executor re-seals blocks using the correct min_base_fee from L1 SystemConfig, producing different extra_data → different block hash → different OutputRoot. The canonical checkpoint becomes unprovable.

The poc demonstrates both impacts end-to-end against a real base reth node

Root Cause 1 — Consolidation discards min_base_fee

// crates/consensus/engine/src/attributes.rs:213-214
let extra_data_decoded = if config.is_jovian_active(block.header.timestamp) {
    JovianExtraData::decode(&block.header.extra_data).map(|(be, bd, _)| (be, bd))
    //                                                               ^^^ discarded

Lines 244-246 compare only (elasticity, denominator) against the L1-derived values. min_base_fee is decoded but thrown away. A block with any arbitrary min_base_fee in its extra_data passes consolidation.

Root Cause 2 — Proof executor uses L1 min_base_fee

The proof executor's seal_block (assemble.rs:79) builds extra_data from L1-derived BasePayloadAttributes — it never copies the canonical block's header. So when the canonical chain accepted min_base_fee = 0 via the consolidation gap, the proof executor re-seals with min_base_fee = 8 gwei → different encoding → different hash → different OutputRoot.

Root Cause 3 — Gossip doesn't check extra_data

I checked crates/consensus/gossip/src/block_validity.rs and it validates timestamp, block hash, signature (must come from unsafe_block_signer), and structural fields. It does not look at extra_data contents at all — no check for min_base_fee, no comparison against L1 SystemConfig. A poisoned block signed by the sequencer propagates to every follower node via p2p gossip, and each node accepts it through the same consolidation path.

So this isn't isolated to one node — every node in the network ends up processing transactions below the governance floor.

Impact

Txpool inclusion below governance floor (proven)

After a poisoned block lands:

  • compute_jovian_base_fee (basefee.rs:51) reads min_base_fee = 0 from the poisoned parent

  • The floor disappears — base_fee_per_gas drops below 8 gwei on the next block

  • Users can submit txs with fees below the L1-configured floor and they get mined

The PoC shows this: after 8 poisoned blocks the base fee drops to ~2.75 gwei, and a tx with max_fee_per_gas = 3 gwei gets accepted and mined — 62.5% below the 8 gwei governance floor.

Proof-system divergence (proven)

Because extra_data in the canonical header is not what the proof executor produces, the block hashes differ, and therefore the OutputRoot (which includes the block hash) differs. The poc computes both using base_protocol::OutputRoot::from_parts and shows they diverge.

What this hits in the proof pipeline:

  • proof/client/src/epilogue.rs:25-30 — rejects the canonical claim with InvalidClaim { computed, claimed }

  • proof/tee/nitro-enclave/src/server.rs:165-166 — TEE validates the epilogue before signing so it can't attest to the canonical root

  • proof/proposer/src/pipeline.rs:1063-1078 — re-fetches canonical root and refuses submission on RootMismatch

  • proof/challenge/src/validator.rs:244-255 — marks games with the proof-exec root invalid against canonical L2 state

No valid AggregateVerifier game can be created for the affected checkpoint range until the code is fixed or the chain is administratively repaired. The bug is fully deterministic — no race, no timing, no probability.

Who triggers this

The consolidation check exists to validate sequencer-produced blocks against L1-derived parameters. That's the fault-proof threat model — catch a misbehaving sequencer. The trigger is a sequencer/engine path that stamps min_base_fee differently from what the L1 SystemConfig says. This could be:

  • A compromised sequencer key

  • A software bug in the sequencer that produces wrong min_base_fee

  • A stale SystemConfig cache on the sequencer side

In all three cases, consolidation should catch it. It doesn't, because of the _ wildcard.

Limitations

  • The PoC demonstrates txpool/block-inclusion and the proof failure predicates locally. It doesn't deploy L1 AggregateVerifier/portal contracts or prove an end-to-end withdrawal failure.

  • The bug only fires when L1 SystemConfig min_base_fee and the stamped header value differ. If both are zero or equal, the path is latent.

  • Manual intervention can repair future operation, but can't make an already-poisoned canonical checkpoint provable without code or config changes.

https://gist.github.com/drawrowfly/833abd506e618724b36cee7b074c72d3

Proof of Concept

I did my best to create runnable poc that shows the problem: https://gist.github.com/drawrowfly/833abd506e618724b36cee7b074c72d3 , there is a shell script that does all the job for convenience

Setup

Run

Or use the provided run_poc.sh script which handles setup automatically.

What the poc does

1

Launches a real Base-reth node with Jovian active at genesis (IPC engine API, full reth, MDBX — no mocks)

2

Mines 3 honest blocks with min_base_fee = 8 gwei

3

Injects poisoned block #4: builds a valid payload via engine API, rewrites extra_data to set min_base_fee = 0, recomputes hash, submits via engine_newPayloadV4 + engine_forkchoiceUpdatedV3

4

Verifies the node accepts it (consolidation bypass)

5

Mines block #5 with poisoned parent to push base fee below the governance floor

6

Submits a signed EIP-1559 tx through public RPC with max_fee_per_gas = 3 gwei (62.5% below the 8 gwei floor)

7

Mines the next block from txpool and verifies the below-floor tx receipt

8

Computes proof-executor's re-sealed header using L1 SystemConfig min_base_fee — same path as assemble.rs:79encode_jovian_eip_1559_params()

9

Computes real Base OutputRoot via base_protocol::OutputRoot::from_parts

10

Asserts extra_data, block hash, and OutputRoot all diverge

Output

Verified on main

Both root causes checked on main:

  • attributes.rs:214_ wildcard still discards min_base_fee

  • util.rs:87-96 — proof executor still uses attributes.min_base_fee

  • block_validity.rs — zero checks for extra_data contents

Was this helpful?