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:
Nullification kills a verifier permanently (lines 594-601):
TEE_VERIFIER.nullify()orZK_VERIFIER.nullify()sets a globalnullifiedflag with no undo mechanism. No further proofs of that type can be verified.resolve()requiresproofCount >= PROOF_THRESHOLD(line 458): With one verifier dead andPROOF_THRESHOLD=2, the game can never accumulate 2 proofs.claimCredit()14-day fallback only works whenexpectedResolution == type(uint64).max(lines 613-617): After a proof is submitted (movingexpectedResolutionaway frommax), the fallback path is disabled, and the function requiresresolvedAt != 0.
Attack Scenarios
Path A — TEE nullified, then ZK proof submitted
Game created with TEE proof →
proofCount=1,expectedResolution = now + 7 daysTEE proof nullified →
proofCount=0,expectedResolution = type(uint64).max,TEE_VERIFIERdeadZK proof submitted via
verifyProposalProof→proofCount=1,expectedResolution = now + 7 daysAfter 7 days:
gameOver() = trueresolve()→proofCount(1) < PROOF_THRESHOLD(2)→ REVERTSclaimCredit()→expectedResolution != max→resolvedAt == 0→ REVERTSBond permanently locked
Path B — ZK nullified after both proofs
TEE proof →
proofCount=1ZK proof →
proofCount=2ZK nullified →
proofCount=1,ZK_VERIFIERdeadCannot re-submit TEE (
AlreadyProven) or ZK (notNullifiedmodifier)resolve()→proofCount(1) < PROOF_THRESHOLD(2)→ REVERTSBond permanently locked
Path C — Challenge ZK nullified
TEE proof →
proofCount=1Challenge with ZK →
proofCount=2,counteredByIntermediateRootIndexPlusOne > 0ZK nullified (proves on-chain root correct) →
proofCount=1,ZK_VERIFIERdeadresolve()→proofCount(1) < PROOF_THRESHOLD(2)→ REVERTSBond 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
anchorGamereset issue or any other previously reported finding.The code explicitly supports
PROOF_THRESHOLD=2via 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:
Link to Proof of Concept
https://gist.github.com/bigfootbjj/4c22790fc3dc4dfb30c06cf55292fa30
Was this helpful?