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

75369 bc medium zk range proof silently truncates derivation on endofsource and commits the original t r n permanent chain split when claimed l2 block number outruns the sequenced l1 bat

Submitted on Apr 28th 2026 at 18:37:08 UTC by @oxeix for Audit Comp | Base Azul

  • Report ID: #75369

  • Report Type: Blockchain/DLT

  • Report severity: Medium

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

  • Impacts:

    • Unintended permanent chain split requiring hard fork (network partition requiring hard fork)

Description

Brief/Intro

advance_to_target (crates/proof/succinct/utils/client/src/client.rs:95-110) silently rewrites its target to the current safe head when the derivation pipeline returns PipelineError::EndOfSource, then returns Ok((safe_head_at_N, R_N, …)). The executor (crates/proof/succinct/utils/client/src/witness/executor.rs:158-179) only checks output_root == claimed_l2_output_root and returns boot_clone (line 198), so the original attacker-supplied claimed_l2_block_number = T flows into BootInfoStruct and onward into AggregationOutputs. The intended "fail loudly on Isthmus / Jovian / Azul" guard at line 106 is dead code: it passes a block number into is_isthmus_active(timestamp: u64) (crates/common/genesis/src/rollup.rs:258-262), and a Base block number (~10^7) is two orders of magnitude smaller than Isthmus's Unix activation timestamp (~10^9), so the comparison is false on every reachable Base block. The proof program therefore produces a valid SP1 proof of the false statement "block T's output root is R_N" for any attacker-chosen T > sequenced tip and any publicly observable canonical root R_N.

Vulnerability Details

End-to-end attack with no other dependencies:

1

1. Observe the canonical state

Attacker observes the canonical safe head N and its output root R_N (visible to anyone running a Base node).

2

2. Submit a witness

Submits a witness with:

  • claimed_l2_block_number = T, where T > N (any value past the sequenced tip; safe_head + 1 works).

  • claimed_l2_output_root = R_N.

  • l1_head = real current L1 head.

3

3. Derivation silently truncates

Inside the SP1 program:

  • get_inputs_for_pipeline accepts the inputs because T ≥ agreed_safe_head.number.

  • advance_to_target(target = T) derives blocks. At block N, the pipeline exhausts L1 batches → PipelineError::EndOfSource.

  • target is silently rewritten to N (client.rs:101). The Isthmus guard (client.rs:106) does nothing because of the timestamp/block-number type confusion, regardless of the active hardfork.

  • Loop top reads tip_cursor.number = N >= tb = N → returns Ok((safe_head_at_N, R_N, …)).

4

4. Executor accepts the root only

Executor: R_N == boot.claimed_l2_output_root. No block-number assertion. Returns boot_clone (the original boot, with T and R_N).

5

5. Boot info and aggregation preserve the attacker’s block number

BootInfoStruct::new(...) (boot.rs:35-57) copies claimed_l2_block_number and claimed_l2_output_root verbatim into l2BlockNumber and l2PostRoot. The range program commits this struct.

The aggregation program (aggregation/src/main.rs:88-118) propagates last_boot_info.l2BlockNumber and last_boot_info.l2PostRoot into AggregationOutputs unchanged — the chain check at lines 29-42 only enforces prev.l2PostRoot == next.l2PreRoot, never re-derives the relationship between l2BlockNumber and l2PostRoot. Final on-chain consumable: keccak256(abi.encodePacked(AggregationOutputs)) with endingL2SequenceNumber = T and l2PostRoot = R_N.

Impact Details

Permanent chain split between L1-finalised state and canonical L2 state, anchored at every block T for which an attacker submits this poison proof

References

https://github.com/base/base/blob/main/crates/proof/succinct/utils/client/src/client.rsL95-110

Proof of Concept

Logs:

Was this helpful?