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

75294 bc low the proposer retries an invalid cached parent instead of refreshing recovery state

Submitted on Apr 28th 2026 at 11:14:23 UTC by @Jolyon for Audit Comp | Base Azul

  • Report ID: #75294

  • Report Type: Blockchain/DLT

  • Report severity: Low

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

  • Impacts:

    • A bug in the respective layer 0/1/2 network code that results in unintended smart contract behavior with no concrete funds at direct risk

Description

Brief/Intro

The proposer in base/base reconstructs its next parent by walking UUID-linked dispute games from the anchor, but it does not verify that the recovered games are still valid parent candidates onchain. If a previously cached chain later becomes invalid because an ancestor resolves CHALLENGER_WINS, is blacklisted, or is retired, the proposer can keep retrying the same stale parent after submission failures instead of refreshing recovery state. This can delay or stall proposal progression until operator intervention or a later cache reset.

Vulnerability Details

The issue is caused by two coupled assumptions in the proposer’s recovery and retry flow.

First, recovery treats UUID reachability as sufficient for parent selection. In base/base, recover_latest_state() reads the anchor root and game_count, then calls forward_walk() to reconstruct the latest tip by repeatedly querying DisputeGameFactory.games(...) with canonical roots and the previous parent address. This logic caches the latest UUID-linked descendant as the next parent candidate, but it does not verify whether that recovered game is still a valid parent candidate onchain at the time of recovery.

Relevant code:

  • Recovery caches the latest UUID-linked descendant without checking current game validity: base/base/crates/proof/proposer/src/pipeline.rs pipeline.rs#L713-L777

  • Forward walk relies only on canonical roots and UUID lookups: base/base/crates/proof/proposer/src/pipeline.rs pipeline.rs#L805-L895

Second, retry logic reuses cached recovery state after failure. In try_submit(), the proposer takes the cached parent_address and uses it directly for the next submission attempt. If submission fails in the ordinary SubmitOutcome::Failed path, handle_submit_result() requeues the same proof but does not invalidate or refresh state.cached_recovery. The next retry therefore uses the same stale parent again.

Relevant code:

  • Cached parent_address is used directly for submission: base/base/crates/proof/proposer/src/pipeline.rs pipeline.rs#L445-L503

  • Failed submissions requeue without refreshing recovery state: base/base/crates/proof/proposer/src/pipeline.rs pipeline.rs#L579-L589

This becomes unsafe because the supplied parent must still be valid onchain. In base/contracts, AggregateVerifier.initialize() rejects a non-anchor parent with InvalidParentGame() if _isValidGame(parentGame) is false. _isValidGame() requires the parent game to still be registered, respected, not blacklisted, not retired, and not already resolved as CHALLENGER_WINS.

Relevant code:

In short, the root cause is that recovery caches a descendant based only on UUID reachability, while retry logic assumes that cached parent remains valid across submission failures. Once a previously cached dispute-game chain later becomes invalid onchain, the proposer can keep retrying the same stale parent instead of forcing a fresh recovery.

Impact Details

This issue is a bug in Base’s L2 proposer / network-side code that results in unintended smart contract behavior with no concrete funds at direct risk.

Once the proposer cache contains a dispute-game descendant whose ancestry later becomes invalid onchain, subsequent proposal attempts can continue reusing that stale parent across failed submissions. Because the normal SubmitOutcome::Failed retry path does not invalidate or refresh cached_recovery, the proposer can keep retrying an invalid parent candidate while the cache key remains unchanged.

The result is not direct fund theft or a proof soundness failure. Instead, the proposer can continue attempting dispute-game creation with an onchain-invalid parent, causing unintended dispute-game submission behavior and delaying creation of the next valid game until recovery state is refreshed, the service is restarted, or another operator action occurs.

This is therefore best classified as a Medium-severity network / proposer logic bug that causes unintended smart contract behavior without concrete funds directly at risk.

Proof of Concept

Repository/revision: base/base@v0.8.0-rc.28

Apply the following additions within the existing #[cfg(test)] mod tests block in crates/proof/proposer/src/pipeline.rs.

Run:

Observed output:

This PoC intentionally mocks the submission failure path; the production invalid-parent rejection is enforced by AggregateVerifier.initialize() and _isValidGame() in base/contracts.

Was this helpful?