75296 sc low permanent freezing of dispute game bonds in aggregateverifier on dual proof chains proof threshold 2 due to unreachable claimcredit fallback
Submitted on Apr 28th 2026 at 11:25:00 UTC by @Oxodus for Audit Comp | Base Azul
Report ID: #75296
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/base/contracts/tree/v8.1.0/src/multiproof
Impacts:
Permanent freezing of funds in the bridge or in dispute game bonds with no available recovery path
Manipulating dispute game bond mechanics to economically grief proposers or prevent legitimate proposals from being submitted
Description
Brief/Intro
On any chain configured with PROOF_THRESHOLD >= 2 (the documented dual-proof end-state), an AggregateVerifier dispute game that reaches the state proofCount == 1 && block.timestamp > expectedResolution becomes permanently stuck. resolve() reverts forever, claimCredit()'s 14-day rescue path is unreachable, and verifyProposalProof() cannot accept a late proof. The proposer's INIT_BOND is permanently locked in DelayedWETH with no available recovery path. The trigger requires no invalid proof; a single 7-day ZK-prover unavailability is sufficient.
Vulnerability Details
The bug is the interaction of three pieces of contracts/src/multiproof/AggregateVerifier.sol. First, resolve() hard-gates on the proof threshold at line 458, reverting NotEnoughProofs whenever fewer proofs have been accepted than PROOF_THRESHOLD:
if (proofCount < PROOF_THRESHOLD) revert NotEnoughProofs();Second, _getDelay() at line 815 maps proofCount == 1 to SLOW_FINALIZATION_DELAY (7 days) rather than type(uint64).max; only proofCount == 0 returns the sentinel:
Third, claimCredit() at lines 613-617 conditions its 14-day rescue branch on expectedResolution == type(uint64).max, otherwise routing to a branch that requires resolvedAt != 0:
When proofCount == 1 and PROOF_THRESHOLD == 2, expectedResolution is set to now + 7 days (not uint64.max), so the 14-day fallback is never taken and claimCredit reverts forever. verifyProposalProof() additionally reverts GameOver after expectedResolution (line 423), so a late second proof cannot rescue the game. The author's intent contradicts the implementation: the comment in challenge() at lines 526-530 reads "If the ZK is nullified, we allow the remaining TEE proof to resolve", but the threshold gate prevents exactly that.
The trapped state is reachable via two independent paths. The first is liveness-only with no attacker and no invalid proof: the proposer initializes a game with a valid TEE proof (proofCount = 1, expectedResolution = now + 7 days); the ZK prover fails to submit within the SLOW window for any benign reason (outage, slow proving, witness-fetcher bug, single-blob censorship); after 7 days the game is permanently stuck. The second is a post-nullification scenario: TEE init, then a ZK challenge brings proofCount to 2, then anyone calls nullify(zkProof, idx, originalRoot) with a ZK proof supporting the original root — _proofRefutedUpdate(ZK) drops proofCount back to 1 and additionally flips Verifier.nullified on the shared ZK_VERIFIER contract. Because nullified is a contract-level flag rather than per-game, the same nullification permanently bricks every future dual proof game on the chain that uses that verifier.
Impact Details
Per-game loss: the full
INIT_BOND(production value set byDisputeGameFactory.setInitBond) is permanently trapped inDelayedWETH.closeGame()requiresresolvedAt != 0, andDelayedWETH.unlockis only callable fromclaimCredit(). No governance recovery path exists currently exists.Liveness collapse: every game whose ZK prover is unavailable for 7+ days reaches the trap state. No adversary required.
Systemic brick after a single nullification since
Verifier.nullifiedis contract level, one successful nullification on any clone permanently bricks every future dual proof game on the chain until the verifier is replaced.Recovery would require a
AggregateVerifierimplementation upgrade and migration ofDisputeGameFactory.gameImpls[621]. In-flight bonds in the old implementation are not recovered by the upgrade.
References
contracts/src/multiproof/AggregateVerifier.solresolve()threshold gate — line 458claimCredit()fallback gate — lines 606-634_getDelay()— lines 815-822_increaseExpectedResolution()— lines 800-813verifyProposalProof()GameOverrevert — line 423Author-intent comment in
challenge()— lines 526-535
contracts/src/multiproof/Verifier.sol—nullifiedis contract-level, not per-game
Proof of Concept
Create a file named BondLockOnDualProof.t.sol and paste the following test
Run the command
The test successfully runs with the following output
Was this helpful?