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

75615 bc medium isthmus withdrawals root check is skipped for in memory parents

Submitted on Apr 30th 2026 at 04:15:19 UTC by @iam0x04 for Audit Comp | Base Azul

  • Report ID: #75615

  • Report Type: Blockchain/DLT

  • Report severity: Medium

  • 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

After Isthmus, header.withdrawals_root is used as the L2ToL1MessagePasser storage root in the L2 output root. The post-execution validator is supposed to bind that header field to the actual post-execution MessagePasser storage root.

In OpEngineValidator::validate_block_post_execution_with_hashed_state, this check is skipped when the parent state cannot be loaded from the canonical provider:

let Ok(state) = self.provider.state_by_block_hash(block.parent_hash()) else {
    return Ok(());
};

This is reachable because engine-tree execution supports parents that are still only in memory and not yet canonical.

Vulnerability Details

  • Skip on missing canonical parent state: crates/execution/node/src/engine.rs:129-145 https://github.com/base/base/blob/e3467a2048881213b56739a54a876efb9c6ea103/crates/execution/node/src/engine.rs#L124-L152

  • Engine tree supports in-memory parent state: crates/execution/engine-tree/src/validator.rs:1215-1222

    https://github.com/base/base/blob/e3467a2048881213b56739a54a876efb9c6ea103/crates/execution/engine-tree/src/validator.rs#L1210-L1223

  • Isthmus output root trusts header withdrawals_root: crates/consensus/engine/src/query.rs:104-110

    https://github.com/base/base/blob/e3467a2048881213b56739a54a876efb9c6ea103/crates/consensus/engine/src/query.rs#L104-L110

Attack path

The malformed child payload must reach the Engine API / payload validation path while its parent is present only in the engine tree. The PoC models that state directly; it does not assume bridge finalization or direct withdrawal theft.

  1. Insert an Isthmus parent payload that exists in the engine tree but is not yet available from the canonical provider.

  2. Insert a child payload with a correct post-execution state root but forged withdrawals_root.

  3. Post-execution validation skips the withdrawals-root check because parent lookup through the canonical provider fails.

  4. If the child becomes canonical, output-root queries commit the forged withdrawals_root.

Impact Details

A child payload can be accepted with a valid EVM state_root but an arbitrary Isthmus withdrawals_root. If that child is later canonicalized, outputAtBlock uses the header withdrawals_root directly as the output root's bridge storage root.

The PoC proves that the in-scope EL accepts and canonicalizes a block whose output root commits to a bridge storage root not produced by execution.

This is not a direct bridge-theft claim, but it is a broken consensus/output-root invariant in the Base-native EL validation path.

Proof of Concept

Put this PoC at crates/execution/runner/tests/poc_005_isthmus_withdrawals_root.rs

Run Instructions

From the repository root:

The PoC intentionally includes a control case to show that the same forged root is rejected when the parent state is available from the canonical provider, and accepted only when the parent is engine-tree-only.

poc_005_isthmus_withdrawals_root.rs

Observed locally: the test passes. It first confirms the same forged withdrawals_root is rejected for a canonical DB-backed parent, then submits the child against an engine-tree-only parent and confirms the block is canonicalized with the valid state_root and forged withdrawals_root. The test also computes the V0 output root hash input and shows the canonicalized header path commits to the forged bridge storage root.

Key observed output:

The first line is the control case: a forged withdrawals_root is rejected when the parent is canonical and DB-backed. The later canonical-chain line is the bug case: the same forged root is accepted after switching the payload parent to an engine-tree-only side parent. The output-root lines show that changing only the bridge storage root from the actual execution result to the forged header value changes the final L2 output root. The accepted header path therefore commits to 0x4242... as the withdrawal/message-passer storage root.

Was this helpful?