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
TransitionTrigger
GameCreation → Unchallengedinitialize() called by DisputeGameFactory
Unchallenged → Challengedchallenge() called before the challenge deadline
Unchallenged → UnchallengedAndValidProofProvidedprove() succeeds
Unchallenged → ResolvedDeadline expires and resolve() is called
Challenged → ChallengedAndValidProofProvidedprove() succeeds
Challenged → ResolvedProve deadline expires and resolve() is called
UnchallengedAndValidProofProvided → Resolvedresolve() is called
ChallengedAndValidProofProvided → Resolvedresolve() 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:

FieldTypeDescription
l2SequenceNumberuint64L2 block number asserted by this game's root claim
parentIndexuint32Index 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 l2SequenceNumber MUST be strictly above the anchor state's l2SequenceNumber.
  • The game's l2SequenceNumber MUST be strictly greater than the parent's l2SequenceNumber.

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 if gameOver() returns true.
  • challenge() MUST revert if the game is not in the Unchallenged state.
  • Only one challenge is allowed per game.
  • Calling challenge() resets the deadline to block.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 as CHALLENGER_WINS.
  • prove() MUST revert if gameOver() returns true (covers an already-submitted proof, an expired deadline, and a resolved game).
  • The verifier call MUST revert for invalid proofs.
  • On success, status transitions to UnchallengedAndValidProofProvided or ChallengedAndValidProofProvided (depending on whether the game was challenged), and gameOver() returns true immediately.

Resolution

Resolution is permissionless. Anyone may call resolve() once gameOver() returns true and the parent game is resolved.

  • resolve() MUST revert if resolvedAt != 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 inherits CHALLENGER_WINS regardless of its own proof status.
  • Otherwise (parent DEFENDER_WINS), the outcome is determined as follows:
Game state at resolutionOutcomeBond distribution
Unchallenged, deadline expiredDEFENDER_WINSProposer recovers initBond
Unchallenged, valid proof providedDEFENDER_WINSProposer recovers initBond; prover receives nothing
Challenged, no proof by deadlineCHALLENGER_WINSChallenger receives initBond + challengerBond
Challenged, valid proof, prover == proposerDEFENDER_WINSProposer recovers initBond + challengerBond
Challenged, valid proof, prover != proposerDEFENDER_WINSProposer recovers initBond; prover receives challengerBond
Parent resolved as CHALLENGER_WINS, child challengedCHALLENGER_WINSChallenger receives initBond + challengerBond
Parent resolved as CHALLENGER_WINS, child unchallengedCHALLENGER_WINSinitBond is sent to address(0)

Bond Distribution

Bond distribution follows a NORMAL or REFUND mode determined by isGameProper (evaluated in closeGame):

ModeConditionEffect
NORMALGame is Proper (registered, not blacklisted, not retired, not paused)Bonds go to winners as described above
REFUNDGame is blacklisted, retired, or otherwise improperinitBond returned to proposer; challengerBond returned to challenger

Complete distribution scenarios:

ScenarioModeProposer getsChallenger getsProver gets
Unchallenged, deadline expiresNORMALinitBond
Unchallenged, proof providedNORMALinitBondnothing
Challenged, no proofNORMALnothinginitBond + challengerBond
Challenged, proof, prover == proposerNORMALinitBond + challengerBondnothing(same)
Challenged, proof, prover != proposerNORMALinitBondnothingchallengerBond
Parent CHALLENGER_WINS, child challengedNORMALnothinginitBond + challengerBond
Parent CHALLENGER_WINS, child unchallengedNORMALsent to address(0)
Game blacklistedREFUNDinitBondchallengerBond
Game retiredREFUNDinitBondchallengerBond

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 AnchorStateRegistry is paused.
  • MUST revert with GameNotResolved if resolvedAt == 0.
  • MUST revert if AnchorStateRegistry.isFinalized(this) returns false.
  • 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 DelayedWETH accordingly.

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.

  1. Records whether the game was open (bondDistributionMode == UNDECIDED) before the call.
  2. Triggers closeGame() if not yet closed. closeGame() handles bond unlocks in DelayedWETH.
  3. Phase 1 — unlock: If recipient still has credit allocated by this game, zeroes it out and calls DelayedWETH.unlock(recipient, amount), then returns. A second call is required to complete the withdrawal once the DelayedWETH delay has elapsed.
  4. Phase 2 — withdraw: If credit has already been zeroed (phase 1 completed), checks DelayedWETH for a pending withdrawal amount. If the amount is zero and the game was open before this call, returns without reverting (so closeGame() state changes persist). If the amount is zero and the game was already closed, reverts with NoCreditToClaim. Otherwise, calls DelayedWETH.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.