75413 sc high zk proof executor derives blobbasefee from base s da footprint header field allowing invalid output roots
Submitted on Apr 29th 2026 at 01:55:46 UTC by @Brainiac5 for Audit Comp | Base Azul
Report ID: #75413
Report Type: Smart Contract
Report severity: High
Target: https://github.com/base/contracts/tree/v8.1.0/src/multiproof
Impacts:
Forging or bypassing TEE or ZK proof verification in AggregateVerifier to finalize an invalid state root on L1
Draining or stealing funds from the L1 bridge portal through invalid withdrawal proofs constructed against a forged finalized state
Circumventing the dispute/challenge mechanism to prevent correction of an invalid proposal before finalization
Description
Brief/Intro
Base disables L2 blob transactions. Its protocol docs explicitly state that BLOBBASEFEE is present but must always push 1 because no L2 blobs are processed. Canonical Base execution implements this rule by hardcoding BlobExcessGasAndPrice { excess_blob_gas: 0, blob_gasprice: 1 } when building the EVM block environment.
The ZK proof executor does not use the same Base-specific rule. For post-Ecotone/Isthmus blocks, it derives the next block's blob gas price from the parent header using Ethereum blob-gas rules. This is wrong after Jovian because Base reuses the header blob_gas_used field to store the block's DA footprint, not Ethereum blob gas.
As a result, an attacker can create a normal high-DA-footprint parent block, then include a transaction in the next block whose contract branches on block.blobbasefee. Canonical Base execution sees 1; the proof executor can see a value greater than 1. The same transaction can therefore produce a different output root in the ZK proof path than it produces on real Base.
Vulnerability Details
Base documents the opcode semantics as:
Canonical Base EVM environment construction follows that rule:
The same hardcoded value is used in the normal header path, next-block path, and payload path:
base/crates/execution/evm/src/lib.rs:81base/crates/execution/evm/src/lib.rs:112base/crates/execution/evm/src/lib.rs:292
The proof executor builds the block environment differently:
Relevant code:
base/crates/proof/executor/src/builder/env.rs:104base/crates/proof/executor/src/builder/env.rs:112
That logic is valid for Ethereum blob-gas headers, but not for Base post-Jovian headers. Jovian intentionally stores DA footprint in blob_gas_used:
Relevant code:
base/crates/execution/evm/src/build.rs:83base/crates/proof/executor/src/builder/assemble.rs:64base/crates/common/evm/src/executor/block_executor.rs:171base/crates/common/evm/src/executor/block_executor.rs:306
The proof executor therefore treats a Base DA-footprint value as Ethereum blob gas. Once the parent DA footprint is above the Prague blob-gas target, BlobExcessGasAndPrice::new() returns a blob gas price greater than 1, contradicting Base's canonical BLOBBASEFEE semantics.
Exploit Path
Impact Details
This is a proof-system soundness failure. The ZK proof can be valid for the proof executor's transition while the transition is not the canonical Base transition.
The dangerous downstream path is the same bridge-root flow used by normal withdrawals:
AggregateVerifier._verifyZkProof()hashes proposer, L1 origin, start root, end root, L2 sequence range, intermediate roots, config hash, andZK_RANGE_HASH.ZK_VERIFIER.verify()accepts the aggregate proof for that journal.A resolved
DEFENDER_WINSgame can become claim-valid inAnchorStateRegistry.OptimismPortal2.proveWithdrawalTransaction()proves the withdrawal storage slot under the game's root claim.OptimismPortal2.finalizeWithdrawalTransactionExternalProof()then releases funds if the game remains claim-valid.
Relevant downstream code:
base/crates/proof/succinct/utils/client/src/client.rs:188base/crates/proof/succinct/utils/client/src/witness/executor.rs:172base/crates/proof/succinct/programs/aggregation/src/main.rs:102contracts/src/multiproof/AggregateVerifier.sol:393contracts/src/multiproof/AggregateVerifier.sol:458contracts/src/multiproof/AggregateVerifier.sol:917contracts/src/multiproof/AggregateVerifier.sol:932contracts/src/L1/OptimismPortal2.sol:385contracts/src/L1/OptimismPortal2.sol:549
This does not require:
Compromising Base-operated infrastructure.
A leaked TEE or ZK key.
An invalid SP1 proof.
A privileged address.
An upstream OP Stack bug.
The emergency ability to blacklist, retire, pause, or replace a verifier may reduce exploitability after detection, but the vulnerability is that Base's in-scope ZK proof executor accepts a state transition that canonical Base execution rejects.
Why This Is In Scope
The Immunefi scope page lists Base Sepolia after the April 20, 2026 Azul activation as the competition environment and includes Offchain Components, Base Azul, and Implementation Contracts. It also calls out proof-system integration and Base-native execution logic as high-value areas. Base mainnet is a planned target deployment, but not the competition environment.
This report is in scope because:
The root cause is in
base/baseproof executor code, not an out-of-scopeactions,devnet,baseup, oretcfolder.The bug is Base-specific: Base's Jovian DA-footprint reuse of
blob_gas_usedconflicts with the proof executor's Ethereum blob-gas calculation.The report does not depend on core Op-Succinct circuit soundness. It depends on Base's integration code feeding the wrong block environment to the execution proof.
The downstream impact crosses into the in-scope
AggregateVerifierandOptimismPortal2verification/finalization boundary.The PoC is runnable locally and does not touch public testnet/mainnet.
This is not an upstream OP Stack issue. The mismatch exists because Base changed the header semantics for Jovian DA footprint while the Base proof executor still treats blob_gas_used as Ethereum blob gas for BLOBBASEFEE.
References
Immunefi Base Azul scope:
https://immunefi.com/audit-competition/audit-comp-base-azul/scope/Base execution spec for disabled L2 blobs and
BLOBBASEFEE:base/docs/specs/pages/protocol/execution/index.md:446-448Canonical Base EVM hardcodes blob gas price to
1:base/crates/execution/evm/src/lib.rs:81-84Canonical next-block env hardcodes blob gas price to
1:base/crates/execution/evm/src/lib.rs:112-115Canonical payload env hardcodes blob gas price to
1:base/crates/execution/evm/src/lib.rs:292-295Proof executor derives blob gas price from parent header:
base/crates/proof/executor/src/builder/env.rs:104-115Jovian header construction stores DA footprint in
blob_gas_used:base/crates/execution/evm/src/build.rs:83-87Proof executor assembled headers also store DA footprint in
blob_gas_used:base/crates/proof/executor/src/builder/assemble.rs:64-65Jovian DA footprint is charged per transaction:
base/crates/common/evm/src/executor/block_executor.rs:171-190Sepolia activates Jovian and Azul:
base/crates/common/chains/src/config.rs:332-333Sepolia max gas limit:
base/crates/common/chains/src/config.rs:356Sepolia multiproof game type and proof threshold:
contract-deployments/sepolia/2026-04-20-activate-multiproof/.env:6-16Apr 23 ZK config update in the
contract-deploymentsrepo onorigin/main:sepolia/2026-04-23-update-zk-config/.envAggregateVerifier ZK journal construction:
contracts/src/multiproof/AggregateVerifier.sol:917-932Portal withdrawal proof/finality checks:
contracts/src/L1/OptimismPortal2.sol:385-414,contracts/src/L1/OptimismPortal2.sol:518-550Runnable state-divergence PoC:
base/crates/proof/succinct/utils/client/tests/audit_blobbasefee_withdrawal_impact.rsRunnable portal-payout PoC:
contracts/test/multiproof/AuditBlobbasefeeForgedWithdrawalEndToEnd.t.sol
Suggested Fix
Build the proof executor's blob_excess_gas_and_price the same way canonical Base execution does:
Do not feed Base's Jovian DA-footprint blob_gas_used header value into EIP-4844/EIP-7691 blob gas pricing.
Add regression tests that:
Assert
prepare_block_env()returnsblob_gasprice == 1for all post-Ecotone Base blocks.Execute a contract that branches on
BLOBBASEFEEunder canonical and proof-executor environments and assert identical state roots.Use a Jovian parent header with nonzero/high DA footprint to prevent this exact regression.
Confidence
Confirmed locally with two runnable PoCs.
PoC 1 proves the exact attacker-controlled state divergence: canonical Base semantics keep BLOBBASEFEE at 1 and no withdrawal marker is created, while the proof-executor environment derives BLOBBASEFEE > 1 from Base's DA-footprint blob_gas_used field and creates the real L2ToL1MessagePasser.sentMessages[withdrawalHash] marker.
PoC 2 proves the downstream bridge impact once that forged output root is accepted by the multiproof game: OptimismPortal2 proves and finalizes the withdrawal and the target receives 1 ETH.
Note: PoC for this report and detailed impact extraction & reproduction was aided with the help of AI.
Proof of Concept
This report has two runnable PoCs.
Fresh-checkout reproduction note:
The two PoC files listed below are audit-only test files and are not present in the upstream repositories. Include them as attachments, or paste them into the exact paths shown before running the commands.
PoC 1 does not require
Cargo.tomlchanges; it uses dependencies already available tobase-proof-succinct-client-utils.The
--config 'target.aarch64-apple-darwin.rustflags=["-C","link-arg=-fuse-ld=ld"]'flag is a local macOS linker workaround. Linux triage environments can omit it if unnecessary.PoC 2 uses the repository's existing
OptimismPortal2FFI helper, the same helper used by existing portal tests.
PoC 1 proves the root-cause execution divergence and the attacker impact on L2 state. The same attacker transaction is executed twice with the same Base Azul precompile table and same L2ToL1MessagePasser runtime bytecode. The only difference is the block blob_gasprice value:
Canonical Base env:
blob_gasprice = 1.Proof-executor env:
blob_gaspriceis derived from a Jovian DA-footprint value in the parent header.
PoC 1 file:
Run PoC 1:
Observed result for PoC 1:
Important PoC 1 assertions:
The attack contract condition is only:
So PoC 1 shows the actual attacker capability:
On canonical Base, no withdrawal marker exists and the attacker contract keeps its
1 ETH.In the proof-executor state, the withdrawal marker exists,
msgNonceincrements, and the message passer receives1 ETH.
PoC 2 proves the downstream L1 payout path once that forged withdrawal output root is accepted by AggregateVerifier.
PoC 2 file:
Run PoC 2:
Observed result for PoC 2:
Important PoC 2 assertions:
PoC 2 intentionally uses a strict local verifier that accepts only the exact expected ZK public-input journal for the forged output root. PoC 1 proves the Base proof-executor path can create that non-canonical withdrawal state from the BLOBBASEFEE mismatch. PoC 2 proves that once the root is accepted by the normal game/portal flow, the bridge payout succeeds.
Full PoC 1 Source
Full PoC 2 Source
Expected vs Actual
Expected:
Every Base execution path, including the ZK proof executor, should expose BLOBBASEFEE == 1 for Cancun-and-later Base blocks because L2 blobs are disabled.
Actual:
Canonical Base execution hardcodes BLOBBASEFEE == 1, but the ZK proof executor derives it from the parent header. After Jovian, the parent header's blob_gas_used is a DA-footprint value, so the proof executor can expose BLOBBASEFEE > 1 for the same block where canonical Base exposes 1.
Was this helpful?