74702 sc low game deadlock and frozen bond when proof threshold 2
Submitted on Apr 24th 2026 at 11:36:17 UTC by @silverologist for Audit Comp | Base Azul
Report ID: #74702
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
Description
Summary
AggregateVerifier explicitly supports PROOF_THRESHOLD == 2, but its state machine does not actually support the case where a game still has only one proof when the single-proof timer expires. At that point, the game becomes permanently stuck: no second proof can ever be added, resolve() can never succeed, and claimCredit() can never release the bond.
The cleanest manifestation is a ZK-first game. A game initialized with a ZK proof gets the normal 7-day single-proof timer. After that timer expires, adding the second proof is blocked by GameOver, resolve() still reverts with NotEnoughProofs, and there is no challenge() escape hatch because challenge() only works when a TEE proof already exists. The dispute bond is therefore permanently frozen.
Detailed Description
The constructor accepts both 1 and 2 as valid proof thresholds:
if (proofThreshold != 1 && proofThreshold != 2) revert InvalidProofThreshold();Whenever a proof is accepted, _proofVerifiedUpdate():
stores the prover,
increments
proofCount,calls
_decreaseExpectedResolution().
For proofCount == 1, _getDelay() returns SLOW_FINALIZATION_DELAY, which is 7 days. That means a game with only one proof gets a finite resolution deadline even if PROOF_THRESHOLD == 2.
Once the 7-day timer expires, the second proof is blocked forever.
verifyProposalProof() begins with:
And gameOver() is:
So once the game reaches its single-proof deadline, the second proof can no longer be added at all.
This is fatal under PROOF_THRESHOLD == 2, because the game still needs a second proof but the contract has already closed the proof-submission window.
Even though the proof-submission window has closed, resolve() still requires:
So the game enters a contradictory state:
it is over for proof-submission purposes,
but it is not complete enough for resolution purposes.
That is the deadlock.
claimCredit() has two modes:
if
expectedResolution != type(uint64).max, the game must already be resolved,otherwise, if
expectedResolution == type(uint64).max, there is a 14-day fallback path.
But in this deadlocked state:
expectedResolutionis finite, because the game still has one proof,resolvedAtis zero, becauseresolve()can never succeed.
So claimCredit() keeps reverting with GameNotResolved() forever.
That is why the bond is permanently frozen.
Concrete Exploit / Failure Sequence
The failure sequence is:
Deploy or configure
AggregateVerifierwithproofThreshold = 2.Create a game initialized with a single ZK proof.
Wait until
SLOW_FINALIZATION_DELAYelapses.Attempt to submit the second proof: revert with
GameOver.Attempt to resolve: revert with
NotEnoughProofs.Wait arbitrarily long.
Attempt to claim the bond: revert with
GameNotResolved.
At that point the game is stuck and the bond is permanently locked.
Impact
A game can become permanently unresolvable and the bond can become permanently unrecoverable.
Under the contest's severity guide, this maps directly to Permanent freezing of funds in the bridge or in dispute game bonds with no available recovery path.
Root Cause
The bug comes from three independent pieces of logic that are internally inconsistent when PROOF_THRESHOLD == 2:
Timer assignment is based on current
proofCount.Proof submission closure is based on
gameOver().Resolution eligibility is based on
proofCount >= PROOF_THRESHOLD.
For one-proof games under threshold two:
the timer says the game is mature after 7 days,
proof submission says the game is closed after 7 days,
resolution says the game is still incomplete forever.
That combination should never be possible in a correct state machine.
Recommended Fix
Once a one-proof game expires under PROOF_THRESHOLD == 2, the contract should move into an explicit terminal refund state.
PoC
Add the following to ./contracts/test/multiproof/AggregateVerifierThresholdTwoPoC.t.sol and run as forge test --match-path test/multiproof/AggregateVerifierThresholdTwoPoC.t.sol:
Observed result from validation:
Was this helpful?