ZK Dispute Game Mechanics
Table of Contents
State Transitions
The game tracks its lifecycle through a GameStatus enum:
enum GameStatus {
Unchallenged, // Initial state after initialize()
UnchallengedAndValidProofProvided,
Challenged,
ChallengedAndValidProofProvided,
Resolved
}
GameCreation ──► Unchallenged ──► Challenged ──► ChallengedAndValidProofProvided ──► Resolved
│ │
│ └─ (deadline expires) ──────────────────────► Resolved
│
├─ (deadline expires) ──────────────────────────────────────► Resolved
│
└──► UnchallengedAndValidProofProvided ─────────────────────► Resolved
| Transition | Trigger |
|---|---|
GameCreation → Unchallenged | initialize() called by DisputeGameFactory |
Unchallenged → Challenged | challenge() called before the challenge deadline |
Unchallenged → UnchallengedAndValidProofProvided | prove() succeeds |
Unchallenged → Resolved | Deadline expires and resolve() is called |
Challenged → ChallengedAndValidProofProvided | prove() succeeds |
Challenged → Resolved | Prove deadline expires and resolve() is called |
UnchallengedAndValidProofProvided → Resolved | resolve() is called |
ChallengedAndValidProofProvided → Resolved | resolve() is called |
Creation
Game creation is fully permissionless. Anyone may call DisputeGameFactory.create() with the
required initBond. The factory deploys an MCP clone, which then validates the proposal.
A game may start from the anchor state by setting parentIndex = type(uint32).max, or it may
reference a parent game.
The _extraData passed to DisputeGameFactory.create() has this layout:
| Field | Type | Description |
|---|---|---|
l2SequenceNumber | uint64 | L2 block number asserted by this game's root claim |
parentIndex | uint32 | Index of the parent game; type(uint32).max if starting from the anchor state |
During initialize(), the game snapshots whether its game type is the currently respected game type
(wasRespectedGameTypeWhenCreated). AnchorStateRegistry.isGameClaimValid() reads this flag when
determining if a game's root claim can advance the anchor state or finalize withdrawals via the
Portal. The snapshot ensures that a game type transition does not retroactively invalidate games
that were created under the previous respected type.
Parent Validation
When a parent is referenced (parentIndex != type(uint32).max), initialize() MUST revert if
any of the following checks fail:
- Parent MUST NOT be blacklisted.
- Parent MUST NOT be retired (i.e.,
createdAt > retirementTimestamp). - Parent MUST be the same game type (
ZK_GAME_TYPE). - Parent MUST NOT have resolved as
CHALLENGER_WINS. - Parent's
l2SequenceNumberMUST be strictly above the anchor state'sl2SequenceNumber. - The game's
l2SequenceNumberMUST be strictly greater than the parent'sl2SequenceNumber.
The isGameRespected check on the parent is intentionally omitted. The respected game type gates
which games can finalize withdrawals (via isGameClaimValid), but MUST NOT prevent in-progress
proposal chains from being completed after a game type transition.
Challenge
Challenging is fully permissionless. Anyone may call challenge() before the challenge deadline.
The call MUST include challengerBond ETH, which challenge() deposits into DelayedWETH on
the caller's behalf.
challenge()MUST revert ifgameOver()returnstrue.challenge()MUST revert if the game is not in theUnchallengedstate.- Only one challenge is allowed per game.
- Calling
challenge()resets the deadline toblock.timestamp + maxProveDuration.
Proving
Proving is fully permissionless. Anyone may call prove(proofBytes) at any point before the
current deadline, regardless of whether the game has been challenged.
prove()MUST revert if the parent game has resolved asCHALLENGER_WINS.prove()MUST revert ifgameOver()returnstrue(covers an already-submitted proof, an expired deadline, and a resolved game).- The verifier call MUST revert for invalid proofs.
- On success,
statustransitions toUnchallengedAndValidProofProvidedorChallengedAndValidProofProvided(depending on whether the game was challenged), andgameOver()returnstrueimmediately.
Resolution
Resolution is permissionless. Anyone may call resolve() once gameOver() returns true and
the parent game is resolved.
resolve()MUST revert ifresolvedAt != 0(i.e., game is already resolved).resolve()MUST revert if the parent game is not yet resolved.- If the parent resolved as
CHALLENGER_WINS, the child inheritsCHALLENGER_WINSregardless of its own proof status. - Otherwise (parent
DEFENDER_WINS), the outcome is determined as follows:
| Game state at resolution | Outcome | Bond distribution |
|---|---|---|
| Unchallenged, deadline expired | DEFENDER_WINS | Proposer recovers initBond |
| Unchallenged, valid proof provided | DEFENDER_WINS | Proposer recovers initBond; prover receives nothing |
| Challenged, no proof by deadline | CHALLENGER_WINS | Challenger receives initBond + challengerBond |
| Challenged, valid proof, prover == proposer | DEFENDER_WINS | Proposer recovers initBond + challengerBond |
| Challenged, valid proof, prover != proposer | DEFENDER_WINS | Proposer recovers initBond; prover receives challengerBond |
Parent resolved as CHALLENGER_WINS, child challenged | CHALLENGER_WINS | Challenger receives initBond + challengerBond |
Parent resolved as CHALLENGER_WINS, child unchallenged | CHALLENGER_WINS | initBond is sent to address(0) |
Bond Distribution
Bond distribution follows a NORMAL or REFUND mode determined by isGameProper (evaluated in
closeGame):
| Mode | Condition | Effect |
|---|---|---|
| NORMAL | Game is Proper (registered, not blacklisted, not retired, not paused) | Bonds go to winners as described above |
| REFUND | Game is blacklisted, retired, or otherwise improper | initBond returned to proposer; challengerBond returned to challenger |
Complete distribution scenarios:
| Scenario | Mode | Proposer gets | Challenger gets | Prover gets |
|---|---|---|---|---|
| Unchallenged, deadline expires | NORMAL | initBond | — | — |
| Unchallenged, proof provided | NORMAL | initBond | — | nothing |
| Challenged, no proof | NORMAL | nothing | initBond + challengerBond | — |
| Challenged, proof, prover == proposer | NORMAL | initBond + challengerBond | nothing | (same) |
| Challenged, proof, prover != proposer | NORMAL | initBond | nothing | challengerBond |
Parent CHALLENGER_WINS, child challenged | NORMAL | nothing | initBond + challengerBond | — |
Parent CHALLENGER_WINS, child unchallenged | NORMAL | sent to address(0) | — | — |
| Game blacklisted | REFUND | initBond | challengerBond | — |
| Game retired | REFUND | initBond | challengerBond | — |
Closing
After resolution, bonds are distributed through a two-phase process identical to the
FaultDisputeGame:
closeGame() (permissionless, also called internally by claimCredit):
- MUST revert if
AnchorStateRegistryis paused. - MUST revert with
GameNotResolvedifresolvedAt == 0. - MUST revert if
AnchorStateRegistry.isFinalized(this)returnsfalse. - If the game has a Valid Claim, registers the game as the new anchor state via
AnchorStateRegistry. - Determines NORMAL or REFUND mode and unlocks bonds in
DelayedWETHaccordingly.
claimCredit(recipient):
Uses a two-phase DelayedWETH withdrawal pattern and MUST NOT revert if the game was open before
the call, allowing op-challenger to use it solely to close the game without knowing in advance
whether the recipient has outstanding credit.
- Records whether the game was open (
bondDistributionMode == UNDECIDED) before the call. - Triggers
closeGame()if not yet closed.closeGame()handles bond unlocks inDelayedWETH. - Phase 1 — unlock: If
recipientstill has credit allocated by this game, zeroes it out and callsDelayedWETH.unlock(recipient, amount), then returns. A second call is required to complete the withdrawal once theDelayedWETHdelay has elapsed. - Phase 2 — withdraw: If credit has already been zeroed (phase 1 completed), checks
DelayedWETHfor a pending withdrawal amount. If the amount is zero and the game was open before this call, returns without reverting (socloseGame()state changes persist). If the amount is zero and the game was already closed, reverts withNoCreditToClaim. Otherwise, callsDelayedWETH.withdraw(recipient)and transfers ETH to the recipient, reverting on transfer failure.
The DelayedWETH delay allows the Guardian to pause and freeze funds if a critical issue is
discovered post-resolution.