Summary
Published Immunefi report #75354.
Base Azul’s deployed multiproof verifier (AggregateVerifier.sol) is configured with BLOCK_INTERVAL = 600 and INTERMEDIATE_BLOCK_INTERVAL = 30. When challenge() or nullify() is called on a 30-block subrange, AggregateVerifier constructs an expected public journal containing a single 32-byte intermediate root (abi.encodePacked(intermediateRootToProve)).
However, the shipped Rust ZK prover service ignores the deployed INTERMEDIATE_BLOCK_INTERVAL and always builds SP1 stdin with a hardcoded DEFAULT_INTERMEDIATE_ROOT_INTERVAL: u64 = 10. For any 30-block challenge or nullification request, the SP1 range program records a root every 10 blocks and commits to a 96-byte public journal (root_at_10 || root_at_20 || root_at_30). Because the public journal digest committed by the ZK proof (96 bytes) never matches the digest constructed by AggregateVerifier (32 bytes), every 30-block ZK challenge or nullification proof generated by the official stack fails on-chain verification.
Root cause
- Deployed multiproof configuration (
base/contract-deployments@e76fde63):sepolia/2026-04-20-activate-multiproof/.env:14-16setsBLOCK_INTERVAL=600,INTERMEDIATE_BLOCK_INTERVAL=30,PROOF_THRESHOLD=1.
- On-chain verifier journal construction (
base/contracts@34dbd708):src/multiproof/AggregateVerifier.sol:520and:586verifyabi.encodePacked(intermediateRootToProve)(1 root = 32 bytes) for a challengedINTERMEDIATE_BLOCK_INTERVAL(30-block) subrange.
- Hardcoded 10-block ZK witness generation (
base/base@4b389f03):crates/proof/succinct/utils/client/src/client.rs:19definespub const DEFAULT_INTERMEDIATE_ROOT_INTERVAL: u64 = 10;.crates/proof/succinct/utils/client/src/client.rs:190pushes an intermediate root wheneverblocks_processed.is_multiple_of(interval).crates/proof/zk/service/src/backends/op_succinct/provider.rs:100-103andcrates/proof/succinct/validity/src/proof_requester.rs:217-219pass the hardcodedDEFAULT_INTERMEDIATE_ROOT_INTERVAL(10) intoget_sp1_stdin().crates/proof/challenge/src/driver.rs:626setsnumber_of_blocks_to_prove: candidate.intermediate_block_interval(30), andzk_prover.proto(ProveBlockRequest) has no field to overrideintermediate_root_interval.
Proof of concept
Running the source-backed verification script from the PoC gist across the three scoped repositories:
Derived public-journal mismatch
30-block challenge/nullify contract roots: 1 root, 32 bytes, sha256=cdcd08b397c27b21b7ea18b562a44cfd1dc947655b80f2eb109fbb0f2cda0fb3
30-block challenge/nullify official ZK roots: 3 roots, 96 bytes, sha256=e80658e9d3057dd6bbab4e672c6d707a59609a3a059fbdb81419c12c89afe937
600-block proposal contract roots: 20 roots, 640 bytes, sha256=acca40aa41124a74aef605356e173326a16da64326946bfceb3c952ad6527882
600-block proposal official ZK roots: 60 roots, 1920 bytes, sha256=ebb5206a724f747be2f8547034ab61cc89f5d6aa7645150e380e495ba47e5524
Result: VULNERABLE. The official ZK path commits to interval=10 journals,
but the deployed AggregateVerifier verifies interval=30 journals.
Impact
When an invalid TEE output-root proposal requires ZK fallback dispute resolution (challenge() or nullify()), the shipped challenger/prover pipeline deterministically produces proofs over a 3-root journal that AggregateVerifier rejects, preventing the official ZK dispute service from challenging invalid proposals.
Recommended fix
Pass the on-chain INTERMEDIATE_BLOCK_INTERVAL (candidate.intermediate_block_interval) through ProveBlockRequest and into get_sp1_stdin() so the SP1 range program commits to the exact checkpoint interval verified by AggregateVerifier.