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

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():

  1. stores the prover,

  2. increments proofCount,

  3. 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:

  • expectedResolution is finite, because the game still has one proof,

  • resolvedAt is zero, because resolve() 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:

  1. Deploy or configure AggregateVerifier with proofThreshold = 2.

  2. Create a game initialized with a single ZK proof.

  3. Wait until SLOW_FINALIZATION_DELAY elapses.

  4. Attempt to submit the second proof: revert with GameOver.

  5. Attempt to resolve: revert with NotEnoughProofs.

  6. Wait arbitrarily long.

  7. 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:

  1. Timer assignment is based on current proofCount.

  2. Proof submission closure is based on gameOver().

  3. 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.

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?