74557 sc low pause blocks verifier nullification but not game resolution
Submitted on Apr 23rd 2026 at 12:30:42 UTC by @silverologist for Audit Comp | Base Azul
Report ID: #74557
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/base/contracts/tree/v8.1.0/src/multiproof
Impacts:
Bypassing the soundness alert mechanism — two conflicting valid proofs of the same type (both TEE or both ZK) fail to trigger automatic game nullification
Description
Summary
When the system is paused, AggregateVerifier can still resolve a game, but the defensive nullify() path is blocked because Verifier::nullify requires the calling game to still be proper. Since AnchorStateRegistry::isGameProper returns false while paused, pause disables the verifier kill switch without actually stopping a game from being resolved.
Description
In multiproof, each AggregateVerifier instance is one dispute game for one proposed root. A proposer creates a game with DisputeGameFactory.createWithInitData(...), supplying an initial proof. The game verifies that proof in AggregateVerifier::initializeWithInitData. Later, another actor may add a second proof with AggregateVerifier::verifyProposalProof. Once enough time passes and the proof threshold is met, AggregateVerifier::resolve decides the winner.
There are two different defensive mechanisms in the game:
AggregateVerifier::challengeis the normal path for disputing a TEE-backed proposal. It is only available when a TEE proof already exists and no ZK proof has been submitted yet, so it cannot be used against a ZK-only game.AggregateVerifier::nullifyis meant for cases where a verifier has justified conflicting outputs and must be invalidated. After checking the conflicting proof,AggregateVerifier::nullifycalls the verifier’s global kill switch throughIVerifier::nullify.
That kill switch is implemented in Verifier::nullify. It only succeeds if the caller is both a respected game and a proper game. Whether a game is proper is determined by AnchorStateRegistry::isGameProper, and that function returns false whenever the system is paused:
So once the system is paused, Verifier::nullify is blocked for all games.
The problem is that pause does not block the rest of the game lifecycle consistently. AggregateVerifier::resolve has no pause check at all. It only checks that the game is still in progress, that the parent has resolved, that the resolution time has passed, and that enough proofs exist. AggregateVerifier::closeGame does check ANCHOR_STATE_REGISTRY.paused(), but that only blocks anchoring while the pause is active. Once the pause is lifted, the already-resolved game can still be closed and promoted.
Pause blocks the same-type verifier-nullification path for both TEE and ZK proofs, but the practical risk is highest for ZK-only games because they do not have the separate TEE->ZK challenge path.
This creates the following sequence:
A ZK-only game is created.
The system is paused.
Defenders find a proof that nullifies the ZK verifier but cannot use
nullify()because it now reverts due to the pause check inAnchorStateRegistry::isGameProper.The game is eventually resolved successfully with the defender winning because
AggregateVerifier::resolvedoes not care that the system is paused.After the pause is lifted,
AggregateVerifier::closeGamepromotes the already-resolved game into the anchor state.
Impact
A root may still be elevated to the anchor state even if the verifier behind the proof is invalid. This happens because the system's paused state disables the nullification process, while still allowing the game's resolution process to continue.
This is a perfect match for the following high severity impact listed as in-scope: Bypassing the soundness alert mechanism — two conflicting valid proofs of the same type (both TEE or both ZK) fail to trigger automatic game nullification.
Recommendation
Allow verifier nullification even when the system is paused.
Proof of Concept
What it demonstrates:
A ZK-only game is created.
Pause is enabled.
nullify()fails throughVerifier.NotProperGame.The same paused game still resolves.
Pause is lifted.
closeGame()succeeds.The resolved game becomes the new anchor.
Run as forge test --match-path test/multiproof/AggregateVerifierPausePoC.t.sol on branch v8.1.0 after adding the following to the POC file:
Was this helpful?