tradi3_ Kurt Landman
Public report archive

Base Azul T3-005

Hardcoded 10-block ZK witness interval makes 30-block challenge proofs unverifiable

Base Azul deploys AggregateVerifier with INTERMEDIATE_BLOCK_INTERVAL=30 (verifying a 1-root, 32-byte journal for 30-block challenges), while the official ZK prover service hardcodes DEFAULT_INTERMEDIATE_ROOT_INTERVAL=10 and commits to a 3-root, 96-byte journal.

Severity
Medium
Outcome
paid
Competition
Audit Comp | Base Azul
Vulnerability class
ZK witness generation / challenge verification
Published

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

  1. Deployed multiproof configuration (base/contract-deployments @ e76fde63):
    • sepolia/2026-04-20-activate-multiproof/.env:14-16 sets BLOCK_INTERVAL=600, INTERMEDIATE_BLOCK_INTERVAL=30, PROOF_THRESHOLD=1.
  2. On-chain verifier journal construction (base/contracts @ 34dbd708):
    • src/multiproof/AggregateVerifier.sol:520 and :586 verify abi.encodePacked(intermediateRootToProve) (1 root = 32 bytes) for a challenged INTERMEDIATE_BLOCK_INTERVAL (30-block) subrange.
  3. Hardcoded 10-block ZK witness generation (base/base @ 4b389f03):
    • crates/proof/succinct/utils/client/src/client.rs:19 defines pub const DEFAULT_INTERMEDIATE_ROOT_INTERVAL: u64 = 10;.
    • crates/proof/succinct/utils/client/src/client.rs:190 pushes an intermediate root whenever blocks_processed.is_multiple_of(interval).
    • crates/proof/zk/service/src/backends/op_succinct/provider.rs:100-103 and crates/proof/succinct/validity/src/proof_requester.rs:217-219 pass the hardcoded DEFAULT_INTERMEDIATE_ROOT_INTERVAL (10) into get_sp1_stdin().
    • crates/proof/challenge/src/driver.rs:626 sets number_of_blocks_to_prove: candidate.intermediate_block_interval (30), and zk_prover.proto (ProveBlockRequest) has no field to override intermediate_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.

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.