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

74911 sc low aggregateverifier bond permanently locked when proof threshold 2 and a verifier is nullified resolve and claimcredit both revert

Submitted on Apr 25th 2026 at 20:57:03 UTC by @Meadowlark14896 for Audit Comp | Base Azul

  • Report ID: #74911

  • Report Type: Smart Contract

  • Report severity: Low

  • Target: https://github.com/base/contracts/tree/v8.1.0/src/multiproof

  • Impacts:

    • Temporary freezing of funds for at least 24 hours (e.g., stuck withdrawal proofs, locked dispute game bonds)

Description

Summary

When PROOF_THRESHOLD=2 (a value explicitly supported by the constructor validation at line 285), if either the TEE or ZK verifier is nullified, the game enters an unresolvable state where the bond is permanently locked. The resolve() function requires proofCount >= PROOF_THRESHOLD, but once a verifier is nullified, proofCount can never reach 2 again. Meanwhile, claimCredit() requires the game to be resolved (since expectedResolution != type(uint64).max), creating a deadlock where the bond cannot be claimed by anyone.

Root Cause

In AggregateVerifier.sol, three conditions create the deadlock:

  1. Nullification kills a verifier permanently (lines 594-601): TEE_VERIFIER.nullify() or ZK_VERIFIER.nullify() sets a global nullified flag with no undo mechanism. No further proofs of that type can be verified.

  2. resolve() requires proofCount >= PROOF_THRESHOLD (line 458): With one verifier dead and PROOF_THRESHOLD=2, the game can never accumulate 2 proofs.

  3. claimCredit() 14-day fallback only works when expectedResolution == type(uint64).max (lines 613-617): After a proof is submitted (moving expectedResolution away from max), the fallback path is disabled, and the function requires resolvedAt != 0.

Attack Scenarios

Path A — TEE nullified, then ZK proof submitted
  1. Game created with TEE proof → proofCount=1, expectedResolution = now + 7 days

  2. TEE proof nullified → proofCount=0, expectedResolution = type(uint64).max, TEE_VERIFIER dead

  3. ZK proof submitted via verifyProposalProofproofCount=1, expectedResolution = now + 7 days

  4. After 7 days: gameOver() = true

  5. resolve()proofCount(1) < PROOF_THRESHOLD(2)REVERTS

  6. claimCredit()expectedResolution != maxresolvedAt == 0REVERTS

  7. Bond permanently locked

Path B — ZK nullified after both proofs
  1. TEE proof → proofCount=1

  2. ZK proof → proofCount=2

  3. ZK nullified → proofCount=1, ZK_VERIFIER dead

  4. Cannot re-submit TEE (AlreadyProven) or ZK (notNullified modifier)

  5. resolve()proofCount(1) < PROOF_THRESHOLD(2)REVERTS

  6. Bond permanently locked

Path C — Challenge ZK nullified
  1. TEE proof → proofCount=1

  2. Challenge with ZK → proofCount=2, counteredByIntermediateRootIndexPlusOne > 0

  3. ZK nullified (proves on-chain root correct) → proofCount=1, ZK_VERIFIER dead

  4. resolve()proofCount(1) < PROOF_THRESHOLD(2)REVERTS

  5. Bond permanently locked

Impact

Permanent freezing of the proposer's bond. The only recovery path is DelayedWETH.recover() which requires the DelayedWETH owner (a multisig) to manually intervene. The bondRecipient (proposer or challenger) has no way to claim their funds.

The code explicitly supports PROOF_THRESHOLD=2 via the constructor validation:

This means the protocol intends to support dual-proof configurations. Any deployment with PROOF_THRESHOLD=2 is vulnerable to permanent bond locking whenever a verifier is nullified.

Proof of Concept

Place in test/multiproof/ and run with forge test --match-test "test_BUG" -vvv:

Why This Is Not a Duplicate

  • This is a code logic bug in AggregateVerifier.sol (src/multiproof/), not a deployment configuration issue.

  • Not listed in the Known Vulnerabilities PDF.

  • Not related to the anchorGame reset issue or any other previously reported finding.

  • The code explicitly supports PROOF_THRESHOLD=2 via constructor validation.

Recommendation

Add a recovery mechanism when a verifier is nullified. For example, reduce PROOF_THRESHOLD dynamically when a verifier is nullified, or allow claimCredit() to use the 14-day fallback regardless of expectedResolution:

https://gist.github.com/bigfootbjj/4c22790fc3dc4dfb30c06cf55292fa30

Was this helpful?