From 001fa1c7269d2a5a943a83b974104f4cadaa7ea5 Mon Sep 17 00:00:00 2001 From: opsuperchain Date: Sun, 21 Dec 2025 01:32:38 +0000 Subject: [PATCH 1/9] feat(contracts): Add DelegatedDisputeGame for per-chain withdrawal proving MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces DelegatedDisputeGame, a minimal dispute game that delegates verification to a SuperFaultDisputeGame. This enables per-chain dispute games that share economic security through a single super game. Key features: - Works with standard DisputeGameFactory.create() - no factory changes needed - Delegates status() and resolvedAt() to the linked SuperGame - Block number verified against output root proof and header RLP - l2SequenceNumber() provides viem-compatible block number access - No bonds required - all economic security comes from the SuperGame CWIA Layout uses extended extraData format containing: - l2BlockNumber, superGameAddress, chainId, OutputRootProof, headerRLP Security: - CEI pattern: initialized flag set before external calls - RLP bounds checking for header decoding - SuperGame address validation Includes 43 passing tests covering: - Game creation via standard factory - Status/resolvedAt delegation to SuperGame - AnchorStateRegistry validation - Resolution flow and edge cases πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/dispute/DelegatedDisputeGame.sol | 433 +++++++ .../test/dispute/DelegatedDisputeGame.t.sol | 1016 +++++++++++++++++ 2 files changed, 1449 insertions(+) create mode 100644 packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol create mode 100644 packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol diff --git a/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol b/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol new file mode 100644 index 00000000000..0f89ecc33f2 --- /dev/null +++ b/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol @@ -0,0 +1,433 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Libraries +import { Clone } from "@solady/utils/Clone.sol"; +import { GameType, GameStatus, Claim, Timestamp, Hash } from "src/dispute/lib/Types.sol"; +import { Types } from "src/libraries/Types.sol"; +import { Hashing } from "src/libraries/Hashing.sol"; +import { RLPReader } from "src/libraries/rlp/RLPReader.sol"; + +// Interfaces +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { ISuperFaultDisputeGame } from "interfaces/dispute/ISuperFaultDisputeGame.sol"; + +/// @title DelegatedDisputeGame +/// @notice A minimal dispute game that delegates verification to a SuperFaultDisputeGame. +/// This enables per-chain dispute games that share economic security through a single super game. +/// The DelegatedDisputeGame has: +/// - No bisection/claims - no internal dispute resolution +/// - No bonds - all economic security is in the SuperGame +/// - Delegated verification - status and resolvedAt come from SuperGame +/// - Block number verification - proves l2BlockNumber matches the output root +/// @dev Uses standard DisputeGameFactory.create() with extended extraData. +/// extraData format: abi.encodePacked( +/// uint256 l2BlockNumber, +/// address superGame, +/// uint256 chainId, +/// bytes32 outputRootProof.version, +/// bytes32 outputRootProof.stateRoot, +/// bytes32 outputRootProof.messagePasserStorageRoot, +/// bytes32 outputRootProof.latestBlockhash, +/// bytes headerRLP +/// ) +/// +/// CWIA Layout (from factory): +/// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +/// β”‚ Byte Range β”‚ Description β”‚ +/// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +/// β”‚ [0, 20) β”‚ Game creator address β”‚ +/// β”‚ [20, 52) β”‚ Root claim (this chain's output root) β”‚ +/// β”‚ [52, 84) β”‚ L1 head hash at creation time β”‚ +/// β”‚ [84, 116) β”‚ L2 block number (viem compatible extraData) β”‚ +/// β”‚ [116, 136) β”‚ Super game address β”‚ +/// β”‚ [136, 168) β”‚ Chain ID β”‚ +/// β”‚ [168, 200) β”‚ OutputRootProof.version β”‚ +/// β”‚ [200, 232) β”‚ OutputRootProof.stateRoot β”‚ +/// β”‚ [232, 264) β”‚ OutputRootProof.messagePasserStorageRoot β”‚ +/// β”‚ [264, 296) β”‚ OutputRootProof.latestBlockhash β”‚ +/// β”‚ [296, ...) β”‚ headerRLP (variable length) β”‚ +/// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +contract DelegatedDisputeGame is Clone, IDisputeGame { + //////////////////////////////////////////////////////////////// + // SIZE CONSTANTS // + //////////////////////////////////////////////////////////////// + + /// @notice Size of an address in bytes. + uint256 internal constant SIZE_ADDRESS = 20; + + /// @notice Size of a bytes32 in bytes. + uint256 internal constant SIZE_BYTES32 = 32; + + /// @notice Size of the OutputRootProof struct (4 * bytes32). + uint256 internal constant SIZE_OUTPUT_ROOT_PROOF = 4 * SIZE_BYTES32; + + //////////////////////////////////////////////////////////////// + // OFFSET CONSTANTS // + //////////////////////////////////////////////////////////////// + // Offsets are chained: OFFSET_B = OFFSET_A + SIZE_A + // This ensures changes propagate correctly. + + /// @notice CWIA offset for game creator address. + uint256 internal constant OFFSET_GAME_CREATOR = 0; + + /// @notice CWIA offset for root claim. + uint256 internal constant OFFSET_ROOT_CLAIM = OFFSET_GAME_CREATOR + SIZE_ADDRESS; // 20 + + /// @notice CWIA offset for L1 head hash. + uint256 internal constant OFFSET_L1_HEAD = OFFSET_ROOT_CLAIM + SIZE_BYTES32; // 52 + + /// @notice CWIA offset for L2 block number (start of extraData). + uint256 internal constant OFFSET_L2_BLOCK_NUMBER = OFFSET_L1_HEAD + SIZE_BYTES32; // 84 + + /// @notice CWIA offset for super game address. + uint256 internal constant OFFSET_SUPER_GAME = OFFSET_L2_BLOCK_NUMBER + SIZE_BYTES32; // 116 + + /// @notice CWIA offset for chain ID. + uint256 internal constant OFFSET_CHAIN_ID = OFFSET_SUPER_GAME + SIZE_ADDRESS; // 136 + + /// @notice CWIA offset for OutputRootProof.version. + uint256 internal constant OFFSET_PROOF_VERSION = OFFSET_CHAIN_ID + SIZE_BYTES32; // 168 + + /// @notice CWIA offset for OutputRootProof.stateRoot. + uint256 internal constant OFFSET_PROOF_STATE_ROOT = OFFSET_PROOF_VERSION + SIZE_BYTES32; // 200 + + /// @notice CWIA offset for OutputRootProof.messagePasserStorageRoot. + uint256 internal constant OFFSET_PROOF_MSG_PASSER_ROOT = OFFSET_PROOF_STATE_ROOT + SIZE_BYTES32; // 232 + + /// @notice CWIA offset for OutputRootProof.latestBlockhash. + uint256 internal constant OFFSET_PROOF_BLOCK_HASH = OFFSET_PROOF_MSG_PASSER_ROOT + SIZE_BYTES32; // 264 + + /// @notice CWIA offset for headerRLP (variable length, must be last). + uint256 internal constant OFFSET_HEADER_RLP = OFFSET_PROOF_BLOCK_HASH + SIZE_BYTES32; // 296 + + /// @notice The index of the block number in an RLP-encoded block header. + /// @dev Consensus encoding reference: + /// https://github.com/paradigmxyz/reth/blob/5f82993c23164ce8ccdc7bf3ae5085205383a5c8/crates/primitives/src/header.rs#L368 + uint256 internal constant HEADER_BLOCK_NUMBER_INDEX = 8; + + //////////////////////////////////////////////////////////////// + // CONSTANTS // + //////////////////////////////////////////////////////////////// + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant VERSION = "1.0.0"; + + //////////////////////////////////////////////////////////////// + // IMMUTABLES // + //////////////////////////////////////////////////////////////// + + /// @notice The game type ID for this delegated dispute game. + GameType internal immutable GAME_TYPE; + + /// @notice The anchor state registry for validation. + IAnchorStateRegistry internal immutable ANCHOR_STATE_REGISTRY; + + //////////////////////////////////////////////////////////////// + // MUTABLE STATE // + //////////////////////////////////////////////////////////////// + + /// @notice Timestamp of the game's creation. + Timestamp public createdAt; + + /// @notice Flag to track initialization. + bool internal initialized; + + /// @notice Whether the respected game type was set when this game was created. + bool internal respectedGameTypeWhenCreated; + + //////////////////////////////////////////////////////////////// + // ERRORS // + //////////////////////////////////////////////////////////////// + + /// @notice Thrown when the game has already been initialized. + error AlreadyInitialized(); + + /// @notice Thrown when bonds are sent (this game accepts no bonds). + error NoBondsAccepted(); + + /// @notice Thrown when the root claim doesn't match the SuperGame's claim for this chain. + error RootClaimMismatch(); + + /// @notice Thrown when the SuperGame doesn't use the same anchor state registry. + error AnchorStateRegistryMismatch(); + + /// @notice Thrown when the game is not in progress (for resolve). + error GameNotInProgress(); + + /// @notice Thrown when the output root proof doesn't hash to the root claim. + error InvalidOutputRootProof(); + + /// @notice Thrown when the header RLP doesn't hash to the block hash in the output root proof. + error InvalidHeaderRLP(); + + /// @notice Thrown when the block number in the header doesn't match l2BlockNumber. + error L2BlockNumberMismatch(); + + /// @notice Thrown when the SuperGame address is zero. + error InvalidSuperGame(); + + //////////////////////////////////////////////////////////////// + // CONSTRUCTOR // + //////////////////////////////////////////////////////////////// + + /// @param _gameType The game type ID for this delegated dispute game. + /// @param _anchorStateRegistry The anchor state registry for validation. + constructor(GameType _gameType, IAnchorStateRegistry _anchorStateRegistry) { + GAME_TYPE = _gameType; + ANCHOR_STATE_REGISTRY = _anchorStateRegistry; + } + + //////////////////////////////////////////////////////////////// + // INITIALIZATION // + //////////////////////////////////////////////////////////////// + + /// @notice Initializes the delegated dispute game. + /// @dev No bonds are accepted. Verification is delegated to the SuperGame. + /// Block number is verified against the output root proof and header RLP. + function initialize() external payable { + // INVARIANT: The game must not have been initialized. + if (initialized) revert AlreadyInitialized(); + + // INVARIANT: No bonds are accepted for delegated games. + if (msg.value != 0) revert NoBondsAccepted(); + + // Mark as initialized before external calls (CEI pattern). + initialized = true; + + // Fetch the super game and verify configuration. + ISuperFaultDisputeGame superGameContract = superGame(); + + // INVARIANT: SuperGame address must not be zero. + if (address(superGameContract) == address(0)) revert InvalidSuperGame(); + + // Verify the root claim matches what the SuperGame has for this chain. + Claim expectedRoot = superGameContract.rootClaimByChainId(chainId()); + if (rootClaim().raw() != expectedRoot.raw()) revert RootClaimMismatch(); + + // Verify the super game uses the same anchor state registry. + if (address(superGameContract.anchorStateRegistry()) != address(ANCHOR_STATE_REGISTRY)) { + revert AnchorStateRegistryMismatch(); + } + + // Verify the block number matches the output root proof and header RLP. + _verifyBlockNumber(); + + // Check if the respected game type matches at creation time. + respectedGameTypeWhenCreated = ANCHOR_STATE_REGISTRY.respectedGameType().raw() == GAME_TYPE.raw(); + + // Set the creation timestamp. + createdAt = Timestamp.wrap(uint64(block.timestamp)); + } + + //////////////////////////////////////////////////////////////// + // DELEGATION METHODS // + //////////////////////////////////////////////////////////////// + + /// @notice Returns the current status of the game. + /// @dev Delegates to the SuperGame's status. + /// @return status_ The current status of the game. + function status() external view returns (GameStatus status_) { + status_ = superGame().status(); + } + + /// @notice Returns the timestamp when the game was resolved. + /// @dev Delegates to the SuperGame's resolvedAt. + /// @return resolvedAt_ The timestamp when the game was resolved. + function resolvedAt() external view returns (Timestamp resolvedAt_) { + resolvedAt_ = superGame().resolvedAt(); + } + + /// @notice Resolves the game. For delegated games, this is a no-op that returns the SuperGame's status. + /// @return status_ The current status of the game. + function resolve() external returns (GameStatus status_) { + // Get status from super game + status_ = superGame().status(); + + // If not yet resolved, revert + if (status_ == GameStatus.IN_PROGRESS) revert GameNotInProgress(); + + emit Resolved(status_); + } + + /// @notice Returns the L2 sequence number (block number for per-chain games). + /// @dev Decodes the L2 block number from the first 32 bytes of extraData. + /// @return l2SequenceNumber_ The L2 block number. + function l2SequenceNumber() external pure returns (uint256 l2SequenceNumber_) { + l2SequenceNumber_ = _l2BlockNumber(); + } + + //////////////////////////////////////////////////////////////// + // IMMUTABLE GETTERS // + //////////////////////////////////////////////////////////////// + + /// @notice Returns the game type. + /// @return gameType_ The game type. + function gameType() public view returns (GameType gameType_) { + gameType_ = GAME_TYPE; + } + + /// @notice Returns the game creator address. + /// @return creator_ The game creator address. + function gameCreator() public pure returns (address creator_) { + creator_ = _getArgAddress(OFFSET_GAME_CREATOR); + } + + /// @notice Returns the root claim (this chain's output root). + /// @return rootClaim_ The root claim. + function rootClaim() public pure returns (Claim rootClaim_) { + rootClaim_ = Claim.wrap(_getArgBytes32(OFFSET_ROOT_CLAIM)); + } + + /// @notice Returns the L1 head hash at game creation. + /// @return l1Head_ The L1 head hash. + function l1Head() public pure returns (Hash l1Head_) { + l1Head_ = Hash.wrap(_getArgBytes32(OFFSET_L1_HEAD)); + } + + /// @notice Returns the extra data (full extraData including proof data). + /// @dev Returns the complete CWIA extraData section for factory lookups. + /// Use l2SequenceNumber() for viem-compatible block number access. + /// @return extraData_ The full extra data bytes. + function extraData() public pure returns (bytes memory extraData_) { + // Calculate the full extraData length from CWIA args. + // Total immutable args length = msg.data.length - offset - 2 byte CWIA suffix + uint256 offset = _getImmutableArgsOffset(); + // Guard against underflow when called on implementation (not clone) + if (msg.data.length <= offset + 2 + OFFSET_L2_BLOCK_NUMBER) { + return extraData_; + } + uint256 immutableArgsLength = msg.data.length - offset - 2; + // extraData starts at OFFSET_L2_BLOCK_NUMBER (84), so its length is total - 84 + uint256 extraDataLength = immutableArgsLength - OFFSET_L2_BLOCK_NUMBER; + extraData_ = _getArgBytes(OFFSET_L2_BLOCK_NUMBER, extraDataLength); + } + + /// @notice Returns the anchor state registry. + /// @return registry_ The anchor state registry. + function anchorStateRegistry() public view returns (IAnchorStateRegistry registry_) { + registry_ = ANCHOR_STATE_REGISTRY; + } + + /// @notice Returns the super game this delegated game is linked to. + /// @return superGame_ The super fault dispute game. + function superGame() public pure returns (ISuperFaultDisputeGame superGame_) { + superGame_ = ISuperFaultDisputeGame(_getArgAddress(OFFSET_SUPER_GAME)); + } + + /// @notice Returns the chain ID this delegated game is for. + /// @return chainId_ The chain ID. + function chainId() public pure returns (uint256 chainId_) { + chainId_ = _getArgUint256(OFFSET_CHAIN_ID); + } + + /// @notice Returns the game data (gameType, rootClaim, extraData). + /// @dev extraData returns the full CWIA extra data for factory lookups. + /// Use l2SequenceNumber() for viem-compatible block number access. + /// @return gameType_ The game type. + /// @return rootClaim_ The root claim. + /// @return extraData_ The full extra data. + function gameData() external view returns (GameType gameType_, Claim rootClaim_, bytes memory extraData_) { + gameType_ = gameType(); + rootClaim_ = rootClaim(); + extraData_ = extraData(); + } + + /// @notice Returns whether the respected game type was set when this game was created. + /// @return respected_ True if the game type was respected at creation. + function wasRespectedGameTypeWhenCreated() external view returns (bool respected_) { + respected_ = respectedGameTypeWhenCreated; + } + + /// @notice Returns the semantic version string. + /// @return version_ The semantic version string. + function version() external pure returns (string memory version_) { + version_ = VERSION; + } + + //////////////////////////////////////////////////////////////// + // INTERNAL HELPERS // + //////////////////////////////////////////////////////////////// + + /// @notice Returns the L2 block number from CWIA args. + /// @return l2BlockNumber_ The L2 block number. + function _l2BlockNumber() internal pure returns (uint256 l2BlockNumber_) { + l2BlockNumber_ = _getArgUint256(OFFSET_L2_BLOCK_NUMBER); + } + + /// @notice Returns the OutputRootProof from CWIA args. + /// @return proof_ The output root proof. + function _getOutputRootProof() internal pure returns (Types.OutputRootProof memory proof_) { + proof_ = Types.OutputRootProof({ + version: _getArgBytes32(OFFSET_PROOF_VERSION), + stateRoot: _getArgBytes32(OFFSET_PROOF_STATE_ROOT), + messagePasserStorageRoot: _getArgBytes32(OFFSET_PROOF_MSG_PASSER_ROOT), + latestBlockhash: _getArgBytes32(OFFSET_PROOF_BLOCK_HASH) + }); + } + + /// @notice Returns the length of the headerRLP from CWIA args. + /// @return length_ The length of the headerRLP in bytes. + function _headerRLPLength() internal pure returns (uint256 length_) { + // Total immutable args length = msg.data.length - offset - 2 byte CWIA suffix + uint256 offset = _getImmutableArgsOffset(); + // Guard against underflow when called on implementation (not clone) + if (msg.data.length <= offset + 2 + OFFSET_HEADER_RLP) { + return 0; + } + uint256 immutableArgsLength = msg.data.length - offset - 2; + length_ = immutableArgsLength - OFFSET_HEADER_RLP; + } + + /// @notice Returns the headerRLP from CWIA args. + /// @return headerRLP_ The RLP-encoded block header. + function _getHeaderRLP() internal pure returns (bytes memory headerRLP_) { + headerRLP_ = _getArgBytes(OFFSET_HEADER_RLP, _headerRLPLength()); + } + + /// @notice Verifies that the l2BlockNumber matches the block number in the output root. + /// @dev Reverts if the output root proof is invalid, header RLP is invalid, or block number mismatches. + function _verifyBlockNumber() internal pure { + // Get the output root proof from CWIA args. + Types.OutputRootProof memory proof = _getOutputRootProof(); + + // Verify: hash(proof) == rootClaim + if (Hashing.hashOutputRootProof(proof) != rootClaim().raw()) { + revert InvalidOutputRootProof(); + } + + // Get the header RLP from CWIA args. + bytes memory headerRLP = _getHeaderRLP(); + + // Verify: keccak256(headerRLP) == proof.latestBlockhash + if (keccak256(headerRLP) != proof.latestBlockhash) { + revert InvalidHeaderRLP(); + } + + // Decode the header RLP to extract the block number. + // Block number is at index 8 in the RLP-encoded block header. + RLPReader.RLPItem[] memory headerContents = RLPReader.readList(RLPReader.toRLPItem(headerRLP)); + + // Sanity check the header has enough elements. + if (headerContents.length <= HEADER_BLOCK_NUMBER_INDEX) revert InvalidHeaderRLP(); + + bytes memory rawBlockNumber = RLPReader.readBytes(headerContents[HEADER_BLOCK_NUMBER_INDEX]); + + // Sanity check the block number string length. + if (rawBlockNumber.length > 32) revert InvalidHeaderRLP(); + + // Convert the raw, left-aligned block number to a uint256. + // SAFETY: The length of `rawBlockNumber` is checked above to ensure it is at most 32 bytes. + uint256 blockNumber; + assembly { + blockNumber := shr(shl(0x03, sub(0x20, mload(rawBlockNumber))), mload(add(rawBlockNumber, 0x20))) + } + + // Verify: extracted block number == l2BlockNumber from extraData + if (blockNumber != _l2BlockNumber()) { + revert L2BlockNumberMismatch(); + } + } +} diff --git a/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol new file mode 100644 index 00000000000..470a6998627 --- /dev/null +++ b/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol @@ -0,0 +1,1016 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +// Testing +import { SuperFaultDisputeGame_TestInit, BaseSuperFaultDisputeGame_TestInit } from "test/dispute/SuperFaultDisputeGame.t.sol"; + +// Libraries +import { GameType, GameStatus, Claim, Timestamp, Hash, VMStatus, VMStatuses } from "src/dispute/lib/Types.sol"; +import { Types } from "src/libraries/Types.sol"; +import { Hashing } from "src/libraries/Hashing.sol"; +import { RLPWriter } from "src/libraries/rlp/RLPWriter.sol"; + +// Contracts +import { DelegatedDisputeGame } from "src/dispute/DelegatedDisputeGame.sol"; + +// Interfaces +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { ISuperFaultDisputeGame } from "interfaces/dispute/ISuperFaultDisputeGame.sol"; + +/// @title DelegatedDisputeGame_Test +/// @notice Tests for the DelegatedDisputeGame contract using standard DisputeGameFactory. +/// Sets up SuperGame with real OutputRootProof hashes (no mocking). +contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { + /// @dev The game type for delegated games. + GameType internal constant DELEGATED_GAME_TYPE = GameType.wrap(100); + + /// @dev The DelegatedDisputeGame implementation. + DelegatedDisputeGame internal delegatedGameImpl; + + /// @dev A created DelegatedDisputeGame proxy. + DelegatedDisputeGame internal delegatedGameProxy; + + /// @dev Stored proof data for chain 5. + Types.OutputRootProof internal proofChain5; + bytes internal headerRLPChain5; + bytes32 internal outputRootChain5; + + /// @dev Stored proof data for chain 6. + Types.OutputRootProof internal proofChain6; + bytes internal headerRLPChain6; + bytes32 internal outputRootChain6; + + /// @dev The root claim of the game. + Claim internal ROOT_CLAIM; + + /// @dev The super root preimage of the game. + Types.SuperRootProof SUPER_ROOT_PROOF; + + /// @dev The preimage of the absolute prestate claim. + bytes internal absolutePrestateData; + + /// @dev The absolute prestate of the trace. + Claim internal absolutePrestate; + + /// @dev A valid l2SequenceNumber that comes after the current anchor root block. + uint256 validl2SequenceNumber; + + function setUp() public override { + absolutePrestateData = abi.encode(0); + absolutePrestate = _changeClaimStatus(Claim.wrap(keccak256(absolutePrestateData)), VMStatuses.UNFINISHED); + + super.setUp(); + + // Get the actual anchor roots. + (, uint256 l2Seqno) = anchorStateRegistry.getAnchorRoot(); + validl2SequenceNumber = l2Seqno + 1; + + // Generate real OutputRootProofs for each chain. + // These will be the actual root claims stored in the SuperGame. + (proofChain5, outputRootChain5, headerRLPChain5) = + _generateOutputRootProof(bytes32(uint256(5)), bytes32(uint256(5000)), abi.encodePacked(uint256(5000))); + + (proofChain6, outputRootChain6, headerRLPChain6) = + _generateOutputRootProof(bytes32(uint256(6)), bytes32(uint256(6000)), abi.encodePacked(uint256(6000))); + + // Build SUPER_ROOT_PROOF with real OutputRootProof hashes. + SUPER_ROOT_PROOF.version = bytes1(uint8(1)); + SUPER_ROOT_PROOF.timestamp = uint64(validl2SequenceNumber); + SUPER_ROOT_PROOF.outputRoots.push(Types.OutputRootWithChainId({ chainId: 5, root: outputRootChain5 })); + SUPER_ROOT_PROOF.outputRoots.push(Types.OutputRootWithChainId({ chainId: 6, root: outputRootChain6 })); + ROOT_CLAIM = Claim.wrap(Hashing.hashSuperRootProof(SUPER_ROOT_PROOF)); + + // Initialize the SuperGame with real output roots. + init({ _rootClaim: ROOT_CLAIM, _absolutePrestate: absolutePrestate, _super: SUPER_ROOT_PROOF }); + + // Deploy the DelegatedDisputeGame implementation with constructor args. + delegatedGameImpl = new DelegatedDisputeGame(DELEGATED_GAME_TYPE, anchorStateRegistry); + + // Register the implementation with the factory. + // Transfer ownership to this test contract first (already done in parent setUp). + disputeGameFactory.setImplementation(DELEGATED_GAME_TYPE, delegatedGameImpl); + + // Set init bond to 0 for delegated games (no bonds). + disputeGameFactory.setInitBond(DELEGATED_GAME_TYPE, 0); + } + + /// @notice Helper to change the VM status byte of a claim. + function _changeClaimStatus(Claim _claim, VMStatus _status) internal pure returns (Claim out_) { + assembly { + out_ := or(and(not(shl(248, 0xFF)), _claim), shl(248, _status)) + } + } + + /// @notice Helper to generate a mock RLP encoded header and output root proof. + /// @param _storageRoot Arbitrary storage root for the proof. + /// @param _withdrawalRoot Arbitrary withdrawal root for the proof. + /// @param _l2BlockNumber The L2 block number to encode in the header. + /// @return proof_ The output root proof. + /// @return root_ The output root (hash of the proof). + /// @return rlp_ The RLP-encoded block header. + function _generateOutputRootProof( + bytes32 _storageRoot, + bytes32 _withdrawalRoot, + bytes memory _l2BlockNumber + ) + internal + pure + returns (Types.OutputRootProof memory proof_, bytes32 root_, bytes memory rlp_) + { + // L2 Block header (minimal mock with block number at index 8) + bytes[] memory rawHeaderRLP = new bytes[](9); + rawHeaderRLP[0] = hex"83FACADE"; + rawHeaderRLP[1] = hex"83FACADE"; + rawHeaderRLP[2] = hex"83FACADE"; + rawHeaderRLP[3] = hex"83FACADE"; + rawHeaderRLP[4] = hex"83FACADE"; + rawHeaderRLP[5] = hex"83FACADE"; + rawHeaderRLP[6] = hex"83FACADE"; + rawHeaderRLP[7] = hex"83FACADE"; + rawHeaderRLP[8] = RLPWriter.writeBytes(_l2BlockNumber); + rlp_ = RLPWriter.writeList(rawHeaderRLP); + + // Output root proof + proof_ = Types.OutputRootProof({ + version: 0, + stateRoot: _storageRoot, + messagePasserStorageRoot: _withdrawalRoot, + latestBlockhash: keccak256(rlp_) + }); + root_ = Hashing.hashOutputRootProof(proof_); + } + + /// @notice Helper to create extended extraData for delegated games with block number proof. + /// @param _l2BlockNumber The L2 block number. + /// @param _superGame The SuperFaultDisputeGame address. + /// @param _chainId The chain ID. + /// @param _proof The output root proof. + /// @param _headerRLP The RLP-encoded block header. + /// @return extraData_ The encoded extra data. + function _createExtendedExtraData( + uint256 _l2BlockNumber, + address _superGame, + uint256 _chainId, + Types.OutputRootProof memory _proof, + bytes memory _headerRLP + ) + internal + pure + returns (bytes memory extraData_) + { + // Format: abi.encodePacked( + // l2BlockNumber, // 32 bytes + // superGame, // 20 bytes + // chainId, // 32 bytes + // proof.version, // 32 bytes + // proof.stateRoot, // 32 bytes + // proof.messagePasserStorageRoot, // 32 bytes + // proof.latestBlockhash, // 32 bytes + // headerRLP // variable + // ) + extraData_ = abi.encodePacked( + _l2BlockNumber, + _superGame, + _chainId, + _proof.version, + _proof.stateRoot, + _proof.messagePasserStorageRoot, + _proof.latestBlockhash, + _headerRLP + ); + } + + /// @notice Helper to create a delegated game for chain 5 (uses stored proof data). + /// @param _l2BlockNumber The L2 block number to claim. + /// @return game_ The created delegated game. + function _createDelegatedGameChain5(uint256 _l2BlockNumber) + internal + returns (DelegatedDisputeGame game_) + { + // For chain 5, the block number in the proof is 5000. + // If _l2BlockNumber matches 5000, it will pass verification. + // Otherwise it will fail with L2BlockNumberMismatch. + bytes memory extraData = _createExtendedExtraData( + _l2BlockNumber, + address(gameProxy), + 5, + proofChain5, + headerRLPChain5 + ); + + game_ = DelegatedDisputeGame( + payable(address(disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData))) + ); + } + + /// @notice Helper to create a delegated game for chain 6 (uses stored proof data). + /// @param _l2BlockNumber The L2 block number to claim. + /// @return game_ The created delegated game. + function _createDelegatedGameChain6(uint256 _l2BlockNumber) + internal + returns (DelegatedDisputeGame game_) + { + bytes memory extraData = _createExtendedExtraData( + _l2BlockNumber, + address(gameProxy), + 6, + proofChain6, + headerRLPChain6 + ); + + game_ = DelegatedDisputeGame( + payable(address(disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain6), extraData))) + ); + } + + /// @notice Tests that the implementation is correctly initialized. + function test_implementation_initialization_succeeds() public view { + assertEq(delegatedGameImpl.gameType().raw(), DELEGATED_GAME_TYPE.raw()); + assertEq(address(delegatedGameImpl.anchorStateRegistry()), address(anchorStateRegistry)); + } + + /// @notice Tests that a delegated game can be created successfully via standard factory. + function test_create_succeeds() public { + // Create delegated game for chain 5 with matching block number (5000). + delegatedGameProxy = _createDelegatedGameChain5(5000); + + // Verify the game was created. + assertTrue(address(delegatedGameProxy) != address(0)); + + // Verify the game's properties. + assertEq(delegatedGameProxy.gameType().raw(), DELEGATED_GAME_TYPE.raw()); + assertEq(delegatedGameProxy.rootClaim().raw(), outputRootChain5); + assertEq(delegatedGameProxy.chainId(), 5); + assertEq(address(delegatedGameProxy.superGame()), address(gameProxy)); + assertEq(address(delegatedGameProxy.anchorStateRegistry()), address(anchorStateRegistry)); + + // Verify extraData contains full proof data (first 32 bytes are l2BlockNumber). + bytes memory fullExtraData = delegatedGameProxy.extraData(); + uint256 decodedBlockNumber; + assembly { + decodedBlockNumber := mload(add(fullExtraData, 32)) + } + assertEq(decodedBlockNumber, 5000); + + // Verify l2SequenceNumber matches. + assertEq(delegatedGameProxy.l2SequenceNumber(), 5000); + } + + /// @notice Tests that create reverts if root claim doesn't match SuperGame. + function test_create_rootClaimMismatch_reverts() public { + uint256 chainId = 5; + uint256 l2BlockNumber = 5000; + + // Use a different root claim than what SuperGame returns. + Claim wrongRootClaim = Claim.wrap(bytes32(uint256(12345))); + + bytes memory extraData = _createExtendedExtraData( + l2BlockNumber, address(gameProxy), chainId, proofChain5, headerRLPChain5 + ); + + vm.expectRevert(DelegatedDisputeGame.RootClaimMismatch.selector); + disputeGameFactory.create(DELEGATED_GAME_TYPE, wrongRootClaim, extraData); + } + + /// @notice Tests that create reverts if bonds are sent. + /// @dev The factory rejects with IncorrectBondAmount since init bond is 0. + function test_create_withBonds_reverts() public { + bytes memory extraData = _createExtendedExtraData( + 5000, address(gameProxy), 5, proofChain5, headerRLPChain5 + ); + + // Factory rejects because init bond is 0, so any value is incorrect + vm.expectRevert(abi.encodeWithSignature("IncorrectBondAmount()")); + disputeGameFactory.create{ value: 1 ether }(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); + } + + /// @notice Tests that status is delegated to SuperGame. + function test_status_delegatesToSuperGame() public { + delegatedGameProxy = _createDelegatedGameChain5(5000); + // Status should match SuperGame's status. + assertEq(uint256(delegatedGameProxy.status()), uint256(gameProxy.status())); + } + + /// @notice Tests that resolvedAt is delegated to SuperGame. + function test_resolvedAt_delegatesToSuperGame() public { + delegatedGameProxy = _createDelegatedGameChain5(5000); + // resolvedAt should match SuperGame's resolvedAt. + assertEq(delegatedGameProxy.resolvedAt().raw(), gameProxy.resolvedAt().raw()); + } + + /// @notice Tests that l2SequenceNumber returns the block number from extraData. + function test_l2SequenceNumber_returnsBlockNumber() public { + delegatedGameProxy = _createDelegatedGameChain5(5000); + assertEq(delegatedGameProxy.l2SequenceNumber(), 5000); + } + + /// @notice Tests that gameData returns correct values. + function test_gameData_returnsCorrectValues() public { + delegatedGameProxy = _createDelegatedGameChain5(5000); + + (GameType gameType_, Claim rootClaim_, bytes memory extraData_) = delegatedGameProxy.gameData(); + + assertEq(gameType_.raw(), DELEGATED_GAME_TYPE.raw()); + assertEq(rootClaim_.raw(), outputRootChain5); + // extraData first 32 bytes are l2BlockNumber (use l2SequenceNumber for viem compatibility) + uint256 decodedBlockNumber; + assembly { + decodedBlockNumber := mload(add(extraData_, 32)) + } + assertEq(decodedBlockNumber, 5000); + } + + /// @notice Tests that the factory's game lookup functions work correctly. + function test_gameAtIndex_succeeds() public { + uint256 gameCountBefore = disputeGameFactory.gameCount(); + + DelegatedDisputeGame proxy = _createDelegatedGameChain5(5000); + + (GameType gameType_, Timestamp timestamp_, IDisputeGame proxy_) = disputeGameFactory.gameAtIndex(gameCountBefore); + + assertEq(gameType_.raw(), DELEGATED_GAME_TYPE.raw()); + assertTrue(timestamp_.raw() > 0); + assertEq(address(proxy_), address(proxy)); + } + + /// @notice Tests that games() lookup works correctly. + function test_games_succeeds() public { + bytes memory extraData = _createExtendedExtraData( + 5000, address(gameProxy), 5, proofChain5, headerRLPChain5 + ); + + IDisputeGame proxy = disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); + + // Look up by game parameters + (IDisputeGame proxy_, Timestamp timestamp_) = + disputeGameFactory.games(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); + + assertTrue(timestamp_.raw() > 0); + assertEq(address(proxy_), address(proxy)); + } + + /// @notice Tests that gameCount is incremented correctly. + function test_gameCount_increments() public { + uint256 gameCountBefore = disputeGameFactory.gameCount(); + + // Create first delegated game for chain 5. + _createDelegatedGameChain5(5000); + assertEq(disputeGameFactory.gameCount(), gameCountBefore + 1); + + // Create second delegated game for chain 6. + _createDelegatedGameChain6(6000); + assertEq(disputeGameFactory.gameCount(), gameCountBefore + 2); + } + + /// @notice Tests that createdAt is set correctly. + function test_createdAt_isSetCorrectly() public { + uint256 expectedTimestamp = block.timestamp; + delegatedGameProxy = _createDelegatedGameChain5(5000); + assertEq(delegatedGameProxy.createdAt().raw(), expectedTimestamp); + } + + /// @notice Tests that wasRespectedGameTypeWhenCreated returns correct value. + function test_wasRespectedGameTypeWhenCreated_returnsCorrectValue() public { + // The delegated game type is not the respected type (SUPER_CANNON is), so this should be false. + delegatedGameProxy = _createDelegatedGameChain5(5000); + assertFalse(delegatedGameProxy.wasRespectedGameTypeWhenCreated()); + } + + /// @notice Tests that version returns the correct string. + function test_version_returnsCorrectString() public view { + assertEq(delegatedGameImpl.version(), "1.0.0"); + } + + /// @notice Tests that gameCreator returns the correct address. + function test_gameCreator_returnsCorrectAddress() public { + delegatedGameProxy = _createDelegatedGameChain5(5000); + // gameCreator should be the caller of create() (this test contract) + assertEq(delegatedGameProxy.gameCreator(), address(this)); + } + + /// @notice Tests that l1Head returns a non-zero value. + function test_l1Head_returnsNonZero() public { + delegatedGameProxy = _createDelegatedGameChain5(5000); + // l1Head should be the parent block hash + assertTrue(delegatedGameProxy.l1Head().raw() != bytes32(0)); + } + + /// @notice Tests creating multiple delegated games for different chains from same SuperGame. + function test_multipleChains_succeeds() public { + // Create delegated game for chain 5 + DelegatedDisputeGame game5 = _createDelegatedGameChain5(5000); + + // Create delegated game for chain 6 + DelegatedDisputeGame game6 = _createDelegatedGameChain6(6000); + + // Verify both games point to same SuperGame + assertEq(address(game5.superGame()), address(gameProxy)); + assertEq(address(game6.superGame()), address(gameProxy)); + + // Verify different chain IDs + assertEq(game5.chainId(), 5); + assertEq(game6.chainId(), 6); + + // Verify different root claims + assertEq(game5.rootClaim().raw(), outputRootChain5); + assertEq(game6.rootClaim().raw(), outputRootChain6); + + // Verify both delegate status to same SuperGame + assertEq(uint256(game5.status()), uint256(gameProxy.status())); + assertEq(uint256(game6.status()), uint256(gameProxy.status())); + } + + /// @notice Tests that create reverts with invalid output root proof. + function test_create_invalidOutputRootProof_reverts() public { + // Create a corrupted proof (different stateRoot than what was used). + Types.OutputRootProof memory corruptedProof = proofChain5; + corruptedProof.stateRoot = bytes32(uint256(999)); + + bytes memory extraData = _createExtendedExtraData( + 5000, address(gameProxy), 5, corruptedProof, headerRLPChain5 + ); + + vm.expectRevert(DelegatedDisputeGame.InvalidOutputRootProof.selector); + disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); + } + + /// @notice Tests that create reverts with invalid header RLP. + function test_create_invalidHeaderRLP_reverts() public { + // Use invalid header RLP (doesn't hash to proof.latestBlockhash). + bytes memory invalidHeaderRLP = hex"DEADBEEF"; + + bytes memory extraData = _createExtendedExtraData( + 5000, address(gameProxy), 5, proofChain5, invalidHeaderRLP + ); + + vm.expectRevert(DelegatedDisputeGame.InvalidHeaderRLP.selector); + disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); + } + + /// @notice Tests that create reverts with mismatched block number. + function test_create_blockNumberMismatch_reverts() public { + // The proof was generated with block number 5000, but we claim 2000. + uint256 wrongBlockNumber = 2000; + + bytes memory extraData = _createExtendedExtraData( + wrongBlockNumber, address(gameProxy), 5, proofChain5, headerRLPChain5 + ); + + vm.expectRevert(DelegatedDisputeGame.L2BlockNumberMismatch.selector); + disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); + } +} + +/// @title DelegatedDisputeGame_TestInit +/// @notice Base test contract for DelegatedDisputeGame tests that creates a delegated game in setUp. +/// Uses real OutputRootProof hashes (no mocking). +contract DelegatedDisputeGame_TestInit is BaseSuperFaultDisputeGame_TestInit { + /// @dev The game type for delegated games. + GameType internal constant DELEGATED_GAME_TYPE = GameType.wrap(100); + + /// @dev The DelegatedDisputeGame implementation. + DelegatedDisputeGame internal delegatedGameImpl; + + /// @dev A created DelegatedDisputeGame proxy. + DelegatedDisputeGame internal delegatedGameProxy; + + /// @dev The chain ID for the delegated game. + uint256 internal delegatedChainId; + + /// @dev The L2 block number for the delegated game (encoded in the proof). + uint256 internal delegatedL2BlockNumber; + + /// @dev Stored proof data for chain 5. + Types.OutputRootProof internal proofChain5; + bytes internal headerRLPChain5; + bytes32 internal outputRootChain5; + + /// @dev Stored proof data for chain 6. + Types.OutputRootProof internal proofChain6; + bytes internal headerRLPChain6; + bytes32 internal outputRootChain6; + + /// @dev The root claim of the game. + Claim internal ROOT_CLAIM; + + /// @dev The super root preimage of the game. + Types.SuperRootProof SUPER_ROOT_PROOF; + + /// @dev The preimage of the absolute prestate claim. + bytes internal absolutePrestateData; + + /// @dev The absolute prestate of the trace. + Claim internal absolutePrestate; + + /// @dev A valid l2SequenceNumber that comes after the current anchor root block. + uint256 validl2SequenceNumber; + + function setUp() public virtual override { + absolutePrestateData = abi.encode(0); + absolutePrestate = _changeClaimStatus(Claim.wrap(keccak256(absolutePrestateData)), VMStatuses.UNFINISHED); + + super.setUp(); + + // Get the actual anchor roots. + (, uint256 l2Seqno) = anchorStateRegistry.getAnchorRoot(); + validl2SequenceNumber = l2Seqno + 1; + + // Generate real OutputRootProofs for each chain. + // Chain 5 has block number 5000, chain 6 has block number 6000. + delegatedChainId = 5; + delegatedL2BlockNumber = 5000; + + (proofChain5, outputRootChain5, headerRLPChain5) = + _generateOutputRootProof( + bytes32(uint256(5)), + bytes32(uint256(delegatedL2BlockNumber)), + abi.encodePacked(delegatedL2BlockNumber) + ); + + (proofChain6, outputRootChain6, headerRLPChain6) = + _generateOutputRootProof(bytes32(uint256(6)), bytes32(uint256(6000)), abi.encodePacked(uint256(6000))); + + // Build SUPER_ROOT_PROOF with real OutputRootProof hashes. + SUPER_ROOT_PROOF.version = bytes1(uint8(1)); + SUPER_ROOT_PROOF.timestamp = uint64(validl2SequenceNumber); + SUPER_ROOT_PROOF.outputRoots.push(Types.OutputRootWithChainId({ chainId: 5, root: outputRootChain5 })); + SUPER_ROOT_PROOF.outputRoots.push(Types.OutputRootWithChainId({ chainId: 6, root: outputRootChain6 })); + ROOT_CLAIM = Claim.wrap(Hashing.hashSuperRootProof(SUPER_ROOT_PROOF)); + + // Initialize the SuperGame with real output roots. + init({ _rootClaim: ROOT_CLAIM, _absolutePrestate: absolutePrestate, _super: SUPER_ROOT_PROOF }); + + // Deploy the DelegatedDisputeGame implementation with constructor args. + delegatedGameImpl = new DelegatedDisputeGame(DELEGATED_GAME_TYPE, anchorStateRegistry); + + // Register the implementation with the factory. + disputeGameFactory.setImplementation(DELEGATED_GAME_TYPE, delegatedGameImpl); + + // Set init bond to 0 for delegated games (no bonds). + disputeGameFactory.setInitBond(DELEGATED_GAME_TYPE, 0); + + // Create a delegated game for chain 5 with the correct block number. + delegatedGameProxy = _createDelegatedGameChain5(delegatedL2BlockNumber); + } + + /// @notice Helper to change the VM status byte of a claim. + function _changeClaimStatus(Claim _claim, VMStatus _status) internal pure returns (Claim out_) { + assembly { + out_ := or(and(not(shl(248, 0xFF)), _claim), shl(248, _status)) + } + } + + /// @notice Helper to generate a mock RLP encoded header and output root proof. + function _generateOutputRootProof( + bytes32 _storageRoot, + bytes32 _withdrawalRoot, + bytes memory _l2BlockNumber + ) + internal + pure + returns (Types.OutputRootProof memory proof_, bytes32 root_, bytes memory rlp_) + { + // L2 Block header (minimal mock with block number at index 8) + bytes[] memory rawHeaderRLP = new bytes[](9); + rawHeaderRLP[0] = hex"83FACADE"; + rawHeaderRLP[1] = hex"83FACADE"; + rawHeaderRLP[2] = hex"83FACADE"; + rawHeaderRLP[3] = hex"83FACADE"; + rawHeaderRLP[4] = hex"83FACADE"; + rawHeaderRLP[5] = hex"83FACADE"; + rawHeaderRLP[6] = hex"83FACADE"; + rawHeaderRLP[7] = hex"83FACADE"; + rawHeaderRLP[8] = RLPWriter.writeBytes(_l2BlockNumber); + rlp_ = RLPWriter.writeList(rawHeaderRLP); + + // Output root proof + proof_ = Types.OutputRootProof({ + version: 0, + stateRoot: _storageRoot, + messagePasserStorageRoot: _withdrawalRoot, + latestBlockhash: keccak256(rlp_) + }); + root_ = Hashing.hashOutputRootProof(proof_); + } + + /// @notice Helper to create extended extraData for delegated games with block number proof. + function _createExtendedExtraData( + uint256 _l2BlockNumber, + address _superGame, + uint256 _chainId, + Types.OutputRootProof memory _proof, + bytes memory _headerRLP + ) + internal + pure + returns (bytes memory extraData_) + { + extraData_ = abi.encodePacked( + _l2BlockNumber, + _superGame, + _chainId, + _proof.version, + _proof.stateRoot, + _proof.messagePasserStorageRoot, + _proof.latestBlockhash, + _headerRLP + ); + } + + /// @notice Helper to create a delegated game for chain 5 (uses stored proof data). + function _createDelegatedGameChain5(uint256 _l2BlockNumber) + internal + returns (DelegatedDisputeGame game_) + { + bytes memory extraData = _createExtendedExtraData( + _l2BlockNumber, + address(gameProxy), + 5, + proofChain5, + headerRLPChain5 + ); + + game_ = DelegatedDisputeGame( + payable(address(disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData))) + ); + } + + /// @notice Helper to create a delegated game for chain 6 (uses stored proof data). + function _createDelegatedGameChain6(uint256 _l2BlockNumber) + internal + returns (DelegatedDisputeGame game_) + { + bytes memory extraData = _createExtendedExtraData( + _l2BlockNumber, + address(gameProxy), + 6, + proofChain6, + headerRLPChain6 + ); + + game_ = DelegatedDisputeGame( + payable(address(disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain6), extraData))) + ); + } + + /// @notice Helper to resolve the SuperGame (uncontested, DEFENDER_WINS). + function _resolveSuperGame() internal { + vm.warp(block.timestamp + 3 days + 12 hours); + gameProxy.resolveClaim(0, 0); + gameProxy.resolve(); + } + + /// @notice Helper to set SuperGame status using vm.store. + /// @param _status The status to set. + function _setSuperGameStatus(GameStatus _status) internal { + // Status is stored at slot 0, offset 16 (as seen in SuperFaultDisputeGame tests). + uint256 slot = uint256(vm.load(address(gameProxy), bytes32(0))); + uint256 offset = 16 << 3; + uint256 mask = 0xFF << offset; + slot = (slot & ~mask) | (uint256(_status) << offset); + vm.store(address(gameProxy), bytes32(0), bytes32(slot)); + } + + /// @dev Counter for generating unique extraData. + uint256 internal gameCounter; + + /// @notice Helper to create a delegated game with unique extraData. + /// @dev Uses a counter to vary the storage root in the proof, making each game unique. + /// @param _chainId The chain ID (5 or 6). + /// @return game_ The created delegated game. + function _createUniqueDelegatedGame(uint256 _chainId) internal returns (DelegatedDisputeGame game_) { + gameCounter++; + + // Get the expected block number for this chain. + uint256 blockNumber = _chainId == 5 ? delegatedL2BlockNumber : 6000; + + // Generate a unique proof by varying the storage root. + (Types.OutputRootProof memory proof, bytes32 outputRoot, bytes memory headerRLP) = + _generateOutputRootProof( + bytes32(gameCounter), // Unique storage root + bytes32(uint256(blockNumber)), + abi.encodePacked(blockNumber) + ); + + // This game uses a different proof than what's in SuperGame, so it won't match rootClaimByChainId. + // For tests that need proper validation, use the stored proof data. + // For tests that just need a unique game, this works. + + bytes memory extraData = _createExtendedExtraData( + blockNumber, + address(gameProxy), + _chainId, + proof, + headerRLP + ); + + // Create the game. Note: this will fail RootClaimMismatch since outputRoot doesn't match SuperGame. + // We need to create games that match the SuperGame's rootClaimByChainId. + game_ = DelegatedDisputeGame( + payable(address(disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRoot), extraData))) + ); + } +} + +/// @title DelegatedDisputeGame_AnchorRegistry_Test +/// @notice Tests for DelegatedDisputeGame's interaction with AnchorStateRegistry. +contract DelegatedDisputeGame_AnchorRegistry_Test is DelegatedDisputeGame_TestInit { + /// @notice Tests that isGameRegistered returns true for a delegated game. + function test_isGameRegistered_succeeds() public view { + assertTrue(anchorStateRegistry.isGameRegistered(delegatedGameProxy)); + } + + /// @notice Tests that isGameRegistered returns false for an unregistered game. + function test_isGameRegistered_unregisteredGame_fails() public { + // Create a game directly (not through factory) - it won't be registered. + DelegatedDisputeGame unregisteredGame = new DelegatedDisputeGame(DELEGATED_GAME_TYPE, anchorStateRegistry); + assertFalse(anchorStateRegistry.isGameRegistered(unregisteredGame)); + } + + /// @notice Tests that isGameProper returns true for a properly created delegated game. + /// @dev Note: isGameProper checks registration, blacklist, retirement - NOT respected status. + /// Use isGameRespected to check if game type was respected at creation. + function test_isGameProper_returnsTrue() public view { + // isGameProper returns true because the game is registered, not blacklisted, not retired. + // This is separate from whether the game type was respected at creation. + assertTrue(anchorStateRegistry.isGameProper(delegatedGameProxy)); + } + + /// @notice Tests that isGameProper returns true when game type is respected. + function test_isGameProper_whenRespected_succeeds() public { + // Set the delegated game type as the respected type. + vm.prank(anchorStateRegistry.superchainConfig().guardian()); + anchorStateRegistry.setRespectedGameType(DELEGATED_GAME_TYPE); + + // Create a new delegated game for chain 6 (chain 5 already used in setUp). + DelegatedDisputeGame newGame = _createDelegatedGameChain6(6000); + + assertTrue(anchorStateRegistry.isGameProper(newGame)); + } + + /// @notice Tests that isGameProper returns false when game is blacklisted. + function test_isGameProper_blacklisted_fails() public { + // Set the delegated game type as respected first. + vm.prank(anchorStateRegistry.superchainConfig().guardian()); + anchorStateRegistry.setRespectedGameType(DELEGATED_GAME_TYPE); + + // Create a new game for chain 6 when respected (chain 5 already used in setUp). + DelegatedDisputeGame newGame = _createDelegatedGameChain6(6000); + + // Verify it's proper before blacklisting. + assertTrue(anchorStateRegistry.isGameProper(newGame)); + + // Blacklist the game. + vm.prank(anchorStateRegistry.superchainConfig().guardian()); + anchorStateRegistry.blacklistDisputeGame(newGame); + + // Now it should not be proper. + assertFalse(anchorStateRegistry.isGameProper(newGame)); + } + + /// @notice Tests that isGameRespected returns the correct value. + function test_isGameRespected_whenRespected_succeeds() public { + // Set the delegated game type as the respected type. + vm.prank(anchorStateRegistry.superchainConfig().guardian()); + anchorStateRegistry.setRespectedGameType(DELEGATED_GAME_TYPE); + + // Create a new delegated game for chain 6 (chain 5 already used in setUp). + DelegatedDisputeGame newGame = _createDelegatedGameChain6(6000); + + assertTrue(anchorStateRegistry.isGameRespected(newGame)); + } + + /// @notice Tests that isGameRespected returns false when not respected. + function test_isGameRespected_whenNotRespected_fails() public view { + // The delegated game was created when its type was not respected. + assertFalse(anchorStateRegistry.isGameRespected(delegatedGameProxy)); + } + + /// @notice Tests that isGameResolved returns correct value based on SuperGame status. + function test_isGameResolved_delegatesToSuperGame() public { + // Before resolution. + assertFalse(anchorStateRegistry.isGameResolved(delegatedGameProxy)); + + // Resolve the SuperGame. + _resolveSuperGame(); + + // After resolution. + assertTrue(anchorStateRegistry.isGameResolved(delegatedGameProxy)); + } + + /// @notice Tests that isGameFinalized returns true after delay. + function test_isGameFinalized_afterDelay_succeeds() public { + // Resolve the SuperGame. + _resolveSuperGame(); + + // Not finalized yet (within delay). + assertFalse(anchorStateRegistry.isGameFinalized(delegatedGameProxy)); + + // Warp past the finality delay. + uint256 finalityDelay = anchorStateRegistry.disputeGameFinalityDelaySeconds(); + vm.warp(block.timestamp + finalityDelay + 1); + + // Now finalized. + assertTrue(anchorStateRegistry.isGameFinalized(delegatedGameProxy)); + } + + /// @notice Tests that isGameFinalized returns false before delay. + function test_isGameFinalized_beforeDelay_fails() public { + // Resolve the SuperGame. + _resolveSuperGame(); + + // Within the delay period. + uint256 finalityDelay = anchorStateRegistry.disputeGameFinalityDelaySeconds(); + vm.warp(block.timestamp + finalityDelay - 1); + + // Not finalized yet. + assertFalse(anchorStateRegistry.isGameFinalized(delegatedGameProxy)); + } +} + +/// @title DelegatedDisputeGame_Resolution_Test +/// @notice Tests for DelegatedDisputeGame resolution flow. +contract DelegatedDisputeGame_Resolution_Test is DelegatedDisputeGame_TestInit { + /// @dev Event emitted when the game is resolved. + event Resolved(GameStatus indexed status); + + /// @notice Tests that status reflects DEFENDER_WINS after SuperGame resolves. + function test_status_afterDefenderWins() public { + // Before resolution. + assertEq(uint256(delegatedGameProxy.status()), uint256(GameStatus.IN_PROGRESS)); + + // Resolve the SuperGame (uncontested = DEFENDER_WINS). + _resolveSuperGame(); + + // After resolution. + assertEq(uint256(delegatedGameProxy.status()), uint256(GameStatus.DEFENDER_WINS)); + } + + /// @notice Tests that status reflects CHALLENGER_WINS when SuperGame is contested. + function test_status_afterChallengerWins() public { + // Use vm.store to set the SuperGame status directly. + _setSuperGameStatus(GameStatus.CHALLENGER_WINS); + + // DelegatedGame should delegate to SuperGame's status. + assertEq(uint256(delegatedGameProxy.status()), uint256(GameStatus.CHALLENGER_WINS)); + } + + /// @notice Tests that resolvedAt matches SuperGame's resolvedAt. + function test_resolvedAt_matchesSuperGame() public { + // Before resolution. + assertEq(delegatedGameProxy.resolvedAt().raw(), 0); + assertEq(gameProxy.resolvedAt().raw(), 0); + + // Resolve the SuperGame. + _resolveSuperGame(); + + // After resolution - both should have same resolvedAt. + assertEq(delegatedGameProxy.resolvedAt().raw(), gameProxy.resolvedAt().raw()); + assertTrue(delegatedGameProxy.resolvedAt().raw() > 0); + } + + /// @notice Tests that resolve() emits event when SuperGame is resolved. + function test_resolve_emitsEvent() public { + // Resolve the SuperGame first. + _resolveSuperGame(); + + // Call resolve on the delegated game - should emit Resolved event. + vm.expectEmit(true, true, true, true); + emit Resolved(GameStatus.DEFENDER_WINS); + delegatedGameProxy.resolve(); + } + + /// @notice Tests that resolve() reverts when SuperGame is not resolved. + function test_resolve_whileInProgress_reverts() public { + // SuperGame is still IN_PROGRESS. + vm.expectRevert(DelegatedDisputeGame.GameNotInProgress.selector); + delegatedGameProxy.resolve(); + } + + /// @notice Tests that resolve returns correct status for CHALLENGER_WINS. + function test_resolve_challengerWins_succeeds() public { + // Set SuperGame to CHALLENGER_WINS. + _setSuperGameStatus(GameStatus.CHALLENGER_WINS); + + // Call resolve - should succeed and return CHALLENGER_WINS. + GameStatus status = delegatedGameProxy.resolve(); + assertEq(uint256(status), uint256(GameStatus.CHALLENGER_WINS)); + } + + /// @notice Tests multiple delegated games share resolution from same SuperGame. + function test_multipleGames_sharedResolution() public { + // Create another delegated game for chain 6. + DelegatedDisputeGame game6 = _createDelegatedGameChain6(6000); + + // Both should be IN_PROGRESS. + assertEq(uint256(delegatedGameProxy.status()), uint256(GameStatus.IN_PROGRESS)); + assertEq(uint256(game6.status()), uint256(GameStatus.IN_PROGRESS)); + + // Resolve the SuperGame. + _resolveSuperGame(); + + // Both should now be DEFENDER_WINS. + assertEq(uint256(delegatedGameProxy.status()), uint256(GameStatus.DEFENDER_WINS)); + assertEq(uint256(game6.status()), uint256(GameStatus.DEFENDER_WINS)); + + // Both should have same resolvedAt. + assertEq(delegatedGameProxy.resolvedAt().raw(), game6.resolvedAt().raw()); + } +} + +/// @title DelegatedDisputeGame_EdgeCases_Test +/// @notice Tests for DelegatedDisputeGame edge cases. +contract DelegatedDisputeGame_EdgeCases_Test is DelegatedDisputeGame_TestInit { + /// @notice Tests that a delegated game can be created after SuperGame is resolved. + function test_create_afterSuperGameResolved_succeeds() public { + // Resolve the SuperGame first. + _resolveSuperGame(); + + // Create a new delegated game for chain 6 (chain 5 already used in setUp). + DelegatedDisputeGame newGame = _createDelegatedGameChain6(6000); + + // Verify the game was created and status is DEFENDER_WINS (delegates to resolved SuperGame). + assertTrue(address(newGame) != address(0)); + assertEq(uint256(newGame.status()), uint256(GameStatus.DEFENDER_WINS)); + } + + /// @notice Tests that initialize reverts if called twice. + function test_initialize_alreadyInitialized_reverts() public { + // The game was already initialized during creation. + vm.expectRevert(DelegatedDisputeGame.AlreadyInitialized.selector); + delegatedGameProxy.initialize(); + } + + /// @notice Tests that creation fails with wrong chain ID (root claim mismatch). + function test_create_mismatchedChainId_reverts() public { + // Get root claim for chain 5 but try to use chain 6's extraData structure. + // This creates extraData pointing to chain 6 but using chain 5's proof. + bytes memory extraData = _createExtendedExtraData( + delegatedL2BlockNumber, + address(gameProxy), + 6, // Wrong chainId - should be 5 to match proofChain5 + proofChain5, + headerRLPChain5 + ); + + // Should revert because rootClaim (for chain 5's output root) doesn't match what SuperGame has for chain 6. + vm.expectRevert(DelegatedDisputeGame.RootClaimMismatch.selector); + disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); + } + + /// @notice Tests that anchor state registry mismatch is caught. + /// @dev Uses vm.mockCall to test this error path (acceptable per testing guidelines). + function test_create_anchorRegistryMismatch_reverts() public { + // Mock the SuperGame's anchorStateRegistry() to return a different address. + address fakeRegistry = address(0xDEAD); + vm.mockCall( + address(gameProxy), + abi.encodeWithSelector(ISuperFaultDisputeGame.anchorStateRegistry.selector), + abi.encode(fakeRegistry) + ); + + // Try to create a delegated game for chain 6 (chain 5 already used in setUp). + bytes memory extraData = _createExtendedExtraData( + 6000, + address(gameProxy), + 6, + proofChain6, + headerRLPChain6 + ); + + vm.expectRevert(DelegatedDisputeGame.AnchorStateRegistryMismatch.selector); + disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain6), extraData); + + // Clear the mock. + vm.clearMockedCalls(); + } + + /// @notice Tests that SuperGame address zero is rejected. + function test_create_invalidSuperGame_reverts() public { + // Create extraData with address(0) as superGame. + bytes memory extraData = _createExtendedExtraData( + 6000, + address(0), // Invalid zero address + 6, + proofChain6, + headerRLPChain6 + ); + + vm.expectRevert(DelegatedDisputeGame.InvalidSuperGame.selector); + disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain6), extraData); + } + + /// @notice Tests that resolve can be called multiple times after resolution. + function test_resolve_multipleCalls_succeeds() public { + // Resolve the SuperGame. + _resolveSuperGame(); + + // First call to resolve. + GameStatus status1 = delegatedGameProxy.resolve(); + assertEq(uint256(status1), uint256(GameStatus.DEFENDER_WINS)); + + // Second call to resolve (should also succeed). + GameStatus status2 = delegatedGameProxy.resolve(); + assertEq(uint256(status2), uint256(GameStatus.DEFENDER_WINS)); + } +} From 726f3acaaf433c1628877f1c39c77fcb13403a7e Mon Sep 17 00:00:00 2001 From: opsuperchain Date: Sun, 21 Dec 2025 03:54:54 +0000 Subject: [PATCH 2/9] feat(contracts): Add SuperGame invalidation propagation to DelegatedDisputeGame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a SuperGame is blacklisted or retired in the superchain AnchorStateRegistry, all DelegatedDisputeGames that reference it are now automatically invalidated. Changes: - Add IDelegatedDisputeGame interface for SuperGame detection - Add try/catch in AnchorStateRegistry.isGameProper() to check SuperGame validity - Remove registry mismatch check from DelegatedDisputeGame (registries will differ) πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../dispute/IDelegatedDisputeGame.sol | 23 ++++++++++++++++ .../src/dispute/AnchorStateRegistry.sol | 21 +++++++++++++++ .../src/dispute/DelegatedDisputeGame.sol | 11 +++----- .../test/dispute/DelegatedDisputeGame.t.sol | 27 ------------------- 4 files changed, 48 insertions(+), 34 deletions(-) create mode 100644 packages/contracts-bedrock/interfaces/dispute/IDelegatedDisputeGame.sol diff --git a/packages/contracts-bedrock/interfaces/dispute/IDelegatedDisputeGame.sol b/packages/contracts-bedrock/interfaces/dispute/IDelegatedDisputeGame.sol new file mode 100644 index 00000000000..7b5390f459b --- /dev/null +++ b/packages/contracts-bedrock/interfaces/dispute/IDelegatedDisputeGame.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { ISuperFaultDisputeGame } from "interfaces/dispute/ISuperFaultDisputeGame.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; + +/// @title IDelegatedDisputeGame +/// @notice Minimal interface for DelegatedDisputeGame, used by AnchorStateRegistry +/// to check SuperGame validity. +interface IDelegatedDisputeGame is IDisputeGame { + /// @notice Returns the super game this delegated game is linked to. + /// @return superGame_ The super fault dispute game. + function superGame() external view returns (ISuperFaultDisputeGame superGame_); + + /// @notice Returns the chain ID this delegated game is for. + /// @return chainId_ The chain ID. + function chainId() external view returns (uint256 chainId_); + + /// @notice Returns the anchor state registry. + /// @return registry_ The anchor state registry. + function anchorStateRegistry() external view returns (IAnchorStateRegistry registry_); +} diff --git a/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol b/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol index cc44cac8ff2..3eb29b740aa 100644 --- a/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol +++ b/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol @@ -16,6 +16,9 @@ import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { ISuperFaultDisputeGame } from "interfaces/dispute/ISuperFaultDisputeGame.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IDelegatedDisputeGame } from "interfaces/dispute/IDelegatedDisputeGame.sol"; /// @custom:proxied true /// @title AnchorStateRegistry @@ -281,6 +284,24 @@ contract AnchorStateRegistry is ProxyAdminOwnedBase, Initializable, Reinitializa return false; } + // For DelegatedDisputeGames, also check SuperGame validity. + // If the SuperGame is blacklisted or retired, the DelegatedDisputeGame is also invalid. + try IDelegatedDisputeGame(address(_game)).superGame() returns (ISuperFaultDisputeGame superGame) { + IAnchorStateRegistry superRegistry = superGame.anchorStateRegistry(); + + // SuperGame must not be blacklisted. + if (superRegistry.isGameBlacklisted(IDisputeGame(address(superGame)))) { + return false; + } + + // SuperGame must not be retired. + if (superRegistry.isGameRetired(IDisputeGame(address(superGame)))) { + return false; + } + } catch { + // Not a DelegatedDisputeGame, that's fine. + } + return true; } diff --git a/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol b/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol index 0f89ecc33f2..723be218597 100644 --- a/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol @@ -151,9 +151,6 @@ contract DelegatedDisputeGame is Clone, IDisputeGame { /// @notice Thrown when the root claim doesn't match the SuperGame's claim for this chain. error RootClaimMismatch(); - /// @notice Thrown when the SuperGame doesn't use the same anchor state registry. - error AnchorStateRegistryMismatch(); - /// @notice Thrown when the game is not in progress (for resolve). error GameNotInProgress(); @@ -207,10 +204,10 @@ contract DelegatedDisputeGame is Clone, IDisputeGame { Claim expectedRoot = superGameContract.rootClaimByChainId(chainId()); if (rootClaim().raw() != expectedRoot.raw()) revert RootClaimMismatch(); - // Verify the super game uses the same anchor state registry. - if (address(superGameContract.anchorStateRegistry()) != address(ANCHOR_STATE_REGISTRY)) { - revert AnchorStateRegistryMismatch(); - } + // Note: We intentionally do NOT check that the SuperGame uses the same AnchorStateRegistry. + // The DelegatedDisputeGame may use a per-chain AnchorStateRegistry while the SuperGame uses + // a superchain-level AnchorStateRegistry. Invalidation propagates through isGameProper() + // which checks the SuperGame's registry for blacklist/retirement status. // Verify the block number matches the output root proof and header RLP. _verifyBlockNumber(); diff --git a/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol index 470a6998627..9b854eddc0a 100644 --- a/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol @@ -958,33 +958,6 @@ contract DelegatedDisputeGame_EdgeCases_Test is DelegatedDisputeGame_TestInit { disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); } - /// @notice Tests that anchor state registry mismatch is caught. - /// @dev Uses vm.mockCall to test this error path (acceptable per testing guidelines). - function test_create_anchorRegistryMismatch_reverts() public { - // Mock the SuperGame's anchorStateRegistry() to return a different address. - address fakeRegistry = address(0xDEAD); - vm.mockCall( - address(gameProxy), - abi.encodeWithSelector(ISuperFaultDisputeGame.anchorStateRegistry.selector), - abi.encode(fakeRegistry) - ); - - // Try to create a delegated game for chain 6 (chain 5 already used in setUp). - bytes memory extraData = _createExtendedExtraData( - 6000, - address(gameProxy), - 6, - proofChain6, - headerRLPChain6 - ); - - vm.expectRevert(DelegatedDisputeGame.AnchorStateRegistryMismatch.selector); - disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain6), extraData); - - // Clear the mock. - vm.clearMockedCalls(); - } - /// @notice Tests that SuperGame address zero is rejected. function test_create_invalidSuperGame_reverts() public { // Create extraData with address(0) as superGame. From a25f8294eb0279d33ed269df4d4178e7d00b65ad Mon Sep 17 00:00:00 2001 From: opsuperchain Date: Sun, 21 Dec 2025 04:14:49 +0000 Subject: [PATCH 3/9] docs: Simplify plan to use normal SystemConfig for superchain level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No special IAnchorRegistryConfig interface needed. Just deploy a standard SystemConfig with zero/minimal values for the superchain-level AnchorStateRegistry. This matches the existing pattern used by SuperFaultDisputeGame tests. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../docs/SUPER_DISPUTE_GAME_FACTORY_PLAN.md | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 packages/contracts-bedrock/docs/SUPER_DISPUTE_GAME_FACTORY_PLAN.md diff --git a/packages/contracts-bedrock/docs/SUPER_DISPUTE_GAME_FACTORY_PLAN.md b/packages/contracts-bedrock/docs/SUPER_DISPUTE_GAME_FACTORY_PLAN.md new file mode 100644 index 00000000000..1b3fd924387 --- /dev/null +++ b/packages/contracts-bedrock/docs/SUPER_DISPUTE_GAME_FACTORY_PLAN.md @@ -0,0 +1,184 @@ +# Super Dispute Game Factory Integration Plan + +## Overview + +This document outlines the plan for implementing a two-factory architecture for DelegatedDisputeGame, where: +1. A **Super Dispute Game Factory** at the superchain level creates `SuperFaultDisputeGame` instances +2. Each chain has its own **DisputeGameFactory** for `DelegatedDisputeGame` instances +3. Invalidation of SuperGames automatically propagates to DelegatedDisputeGames + +--- + +## Final Architecture + +### Key Insight: Just Deploy a Normal SystemConfig + +Both superchain and per-chain levels use the **same AnchorStateRegistry contract** initialized with a **normal SystemConfig**. For the superchain level, we simply deploy a SystemConfig with zero/minimal values - no special interface or contract changes needed. + +This matches the existing pattern used by SuperFaultDisputeGame tests, which already deploy a real SystemConfig and point the AnchorStateRegistry at it. + +### Visual Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ SUPERCHAIN LEVEL β”‚ +β”‚ β”‚ +β”‚ SystemConfig (with zero/minimal values) β”‚ +β”‚ β”œβ”€β”€ Points to SuperchainConfig for paused()/guardian() β”‚ +β”‚ └── Standard SystemConfig, no modifications needed β”‚ +β”‚ β”‚ +β”‚ AnchorStateRegistry β”‚ +β”‚ β”œβ”€β”€ initialized with SystemConfig (standard) β”‚ +β”‚ β”œβ”€β”€ blacklist/retirement for SuperGames β”‚ +β”‚ └── respectedGameType for SuperGames β”‚ +β”‚ β”‚ +β”‚ DisputeGameFactory (SuperDisputeGameFactory) β”‚ +β”‚ β”œβ”€β”€ creates SuperFaultDisputeGame β”‚ +β”‚ └── creates SuperPermissionedDisputeGame β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ isGameProper() checks SuperGame validity + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ PER-CHAIN LEVEL β”‚ +β”‚ β”‚ +β”‚ SystemConfig (per-chain, standard) β”‚ +β”‚ β”‚ +β”‚ AnchorStateRegistry β”‚ +β”‚ β”œβ”€β”€ initialized with SystemConfig β”‚ +β”‚ β”œβ”€β”€ blacklist/retirement for per-chain games β”‚ +β”‚ β”œβ”€β”€ isGameProper() checks SuperGame validity ← IMPLEMENTED β”‚ +β”‚ └── anchor state for this chain β”‚ +β”‚ β”‚ +β”‚ DisputeGameFactory β”‚ +β”‚ └── creates DelegatedDisputeGame β”‚ +β”‚ β”‚ +β”‚ DelegatedDisputeGame β”‚ +β”‚ β”œβ”€β”€ delegates status()/resolvedAt() to SuperGame β”‚ +β”‚ └── NO registry mismatch check (registries are different) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Implementation Status + +### Completed + +#### 1. SuperGame Validity Check in AnchorStateRegistry + +**File:** `src/dispute/AnchorStateRegistry.sol` + +Added try/catch in `isGameProper()` to check SuperGame validity: + +```solidity +// For DelegatedDisputeGames, also check SuperGame validity. +// If the SuperGame is blacklisted or retired, the DelegatedDisputeGame is also invalid. +try IDelegatedDisputeGame(address(_game)).superGame() returns (ISuperFaultDisputeGame superGame) { + IAnchorStateRegistry superRegistry = superGame.anchorStateRegistry(); + + // SuperGame must not be blacklisted. + if (superRegistry.isGameBlacklisted(IDisputeGame(address(superGame)))) { + return false; + } + + // SuperGame must not be retired. + if (superRegistry.isGameRetired(IDisputeGame(address(superGame)))) { + return false; + } +} catch { + // Not a DelegatedDisputeGame, that's fine. +} +``` + +#### 2. Created IDelegatedDisputeGame Interface + +**File:** `interfaces/dispute/IDelegatedDisputeGame.sol` + +Minimal interface for SuperGame detection: + +```solidity +interface IDelegatedDisputeGame is IDisputeGame { + function superGame() external view returns (ISuperFaultDisputeGame superGame_); + function chainId() external view returns (uint256 chainId_); + function anchorStateRegistry() external view returns (IAnchorStateRegistry registry_); +} +``` + +#### 3. Updated DelegatedDisputeGame + +**File:** `src/dispute/DelegatedDisputeGame.sol` + +Removed the registry mismatch check since registries will be different (per-chain vs superchain): + +```solidity +// Note: We intentionally do NOT check that the SuperGame uses the same AnchorStateRegistry. +// The DelegatedDisputeGame may use a per-chain AnchorStateRegistry while the SuperGame uses +// a superchain-level AnchorStateRegistry. Invalidation propagates through isGameProper() +// which checks the SuperGame's registry for blacklist/retirement status. +``` + +### Remaining (Deployment) + +Deploy superchain-level infrastructure: +1. Deploy a SystemConfig with zero/minimal values for superchain use +2. Deploy AnchorStateRegistry initialized with that SystemConfig +3. Deploy DisputeGameFactory (SuperDisputeGameFactory) +4. Register SuperFaultDisputeGame implementation + +--- + +## Invalidation Propagation + +### How It Works + +When Guardian blacklists or retires a SuperGame in the superchain AnchorStateRegistry: + +1. SuperGame is marked as blacklisted/retired in superchain AnchorStateRegistry +2. Per-chain AnchorStateRegistry.isGameProper(delegatedGame) is called +3. The try/catch detects it's a DelegatedDisputeGame +4. It queries the SuperGame's registry (superchain AnchorStateRegistry) +5. If SuperGame is blacklisted/retired, returns false +6. Portal's withdrawal validation fails + +### What Gets Checked + +| Check | Where | Scope | +|-------|-------|-------| +| DelegatedGame blacklisted | Per-chain registry | Single game | +| DelegatedGame retired | Per-chain registry | Games before timestamp | +| SuperGame blacklisted | Superchain registry | Propagates to all DelegatedGames | +| SuperGame retired | Superchain registry | Propagates to all DelegatedGames | +| Global pause | SuperchainConfig | All games everywhere | + +--- + +## Files Modified + +| File | Change Type | Description | +|------|-------------|-------------| +| `interfaces/dispute/IDelegatedDisputeGame.sol` | **NEW** | Minimal interface for SuperGame detection | +| `src/dispute/AnchorStateRegistry.sol` | Modified | Added SuperGame validity check in isGameProper() | +| `src/dispute/DelegatedDisputeGame.sol` | Modified | Removed registry mismatch check | + +--- + +## Testing Strategy + +### Unit Tests + +1. isGameProper() correctly detects DelegatedDisputeGame and checks SuperGame +2. Non-DelegatedDisputeGame passes through isGameProper() normally + +### Integration Tests + +1. SuperGame blacklist propagates to DelegatedDisputeGames +2. SuperGame retirement propagates to DelegatedDisputeGames +3. Per-chain blacklist works independently of SuperGame status +4. Full withdrawal flow with new architecture + +--- + +## Summary + +The key insight is that we don't need any special interface changes. The superchain level just deploys a normal SystemConfig (with zero/minimal values) and a standard AnchorStateRegistry. The only code change needed was adding the try/catch in `isGameProper()` to propagate SuperGame invalidation to DelegatedDisputeGames. From 4e54a893bd7089e81b8fff1cced6e102ecad13a3 Mon Sep 17 00:00:00 2001 From: opsuperchain Date: Sun, 21 Dec 2025 04:15:12 +0000 Subject: [PATCH 4/9] Revert "docs: Simplify plan to use normal SystemConfig for superchain level" This reverts commit a25f8294eb0279d33ed269df4d4178e7d00b65ad. --- .../docs/SUPER_DISPUTE_GAME_FACTORY_PLAN.md | 184 ------------------ 1 file changed, 184 deletions(-) delete mode 100644 packages/contracts-bedrock/docs/SUPER_DISPUTE_GAME_FACTORY_PLAN.md diff --git a/packages/contracts-bedrock/docs/SUPER_DISPUTE_GAME_FACTORY_PLAN.md b/packages/contracts-bedrock/docs/SUPER_DISPUTE_GAME_FACTORY_PLAN.md deleted file mode 100644 index 1b3fd924387..00000000000 --- a/packages/contracts-bedrock/docs/SUPER_DISPUTE_GAME_FACTORY_PLAN.md +++ /dev/null @@ -1,184 +0,0 @@ -# Super Dispute Game Factory Integration Plan - -## Overview - -This document outlines the plan for implementing a two-factory architecture for DelegatedDisputeGame, where: -1. A **Super Dispute Game Factory** at the superchain level creates `SuperFaultDisputeGame` instances -2. Each chain has its own **DisputeGameFactory** for `DelegatedDisputeGame` instances -3. Invalidation of SuperGames automatically propagates to DelegatedDisputeGames - ---- - -## Final Architecture - -### Key Insight: Just Deploy a Normal SystemConfig - -Both superchain and per-chain levels use the **same AnchorStateRegistry contract** initialized with a **normal SystemConfig**. For the superchain level, we simply deploy a SystemConfig with zero/minimal values - no special interface or contract changes needed. - -This matches the existing pattern used by SuperFaultDisputeGame tests, which already deploy a real SystemConfig and point the AnchorStateRegistry at it. - -### Visual Architecture - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ SUPERCHAIN LEVEL β”‚ -β”‚ β”‚ -β”‚ SystemConfig (with zero/minimal values) β”‚ -β”‚ β”œβ”€β”€ Points to SuperchainConfig for paused()/guardian() β”‚ -β”‚ └── Standard SystemConfig, no modifications needed β”‚ -β”‚ β”‚ -β”‚ AnchorStateRegistry β”‚ -β”‚ β”œβ”€β”€ initialized with SystemConfig (standard) β”‚ -β”‚ β”œβ”€β”€ blacklist/retirement for SuperGames β”‚ -β”‚ └── respectedGameType for SuperGames β”‚ -β”‚ β”‚ -β”‚ DisputeGameFactory (SuperDisputeGameFactory) β”‚ -β”‚ β”œβ”€β”€ creates SuperFaultDisputeGame β”‚ -β”‚ └── creates SuperPermissionedDisputeGame β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”‚ isGameProper() checks SuperGame validity - β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ PER-CHAIN LEVEL β”‚ -β”‚ β”‚ -β”‚ SystemConfig (per-chain, standard) β”‚ -β”‚ β”‚ -β”‚ AnchorStateRegistry β”‚ -β”‚ β”œβ”€β”€ initialized with SystemConfig β”‚ -β”‚ β”œβ”€β”€ blacklist/retirement for per-chain games β”‚ -β”‚ β”œβ”€β”€ isGameProper() checks SuperGame validity ← IMPLEMENTED β”‚ -β”‚ └── anchor state for this chain β”‚ -β”‚ β”‚ -β”‚ DisputeGameFactory β”‚ -β”‚ └── creates DelegatedDisputeGame β”‚ -β”‚ β”‚ -β”‚ DelegatedDisputeGame β”‚ -β”‚ β”œβ”€β”€ delegates status()/resolvedAt() to SuperGame β”‚ -β”‚ └── NO registry mismatch check (registries are different) β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - ---- - -## Implementation Status - -### Completed - -#### 1. SuperGame Validity Check in AnchorStateRegistry - -**File:** `src/dispute/AnchorStateRegistry.sol` - -Added try/catch in `isGameProper()` to check SuperGame validity: - -```solidity -// For DelegatedDisputeGames, also check SuperGame validity. -// If the SuperGame is blacklisted or retired, the DelegatedDisputeGame is also invalid. -try IDelegatedDisputeGame(address(_game)).superGame() returns (ISuperFaultDisputeGame superGame) { - IAnchorStateRegistry superRegistry = superGame.anchorStateRegistry(); - - // SuperGame must not be blacklisted. - if (superRegistry.isGameBlacklisted(IDisputeGame(address(superGame)))) { - return false; - } - - // SuperGame must not be retired. - if (superRegistry.isGameRetired(IDisputeGame(address(superGame)))) { - return false; - } -} catch { - // Not a DelegatedDisputeGame, that's fine. -} -``` - -#### 2. Created IDelegatedDisputeGame Interface - -**File:** `interfaces/dispute/IDelegatedDisputeGame.sol` - -Minimal interface for SuperGame detection: - -```solidity -interface IDelegatedDisputeGame is IDisputeGame { - function superGame() external view returns (ISuperFaultDisputeGame superGame_); - function chainId() external view returns (uint256 chainId_); - function anchorStateRegistry() external view returns (IAnchorStateRegistry registry_); -} -``` - -#### 3. Updated DelegatedDisputeGame - -**File:** `src/dispute/DelegatedDisputeGame.sol` - -Removed the registry mismatch check since registries will be different (per-chain vs superchain): - -```solidity -// Note: We intentionally do NOT check that the SuperGame uses the same AnchorStateRegistry. -// The DelegatedDisputeGame may use a per-chain AnchorStateRegistry while the SuperGame uses -// a superchain-level AnchorStateRegistry. Invalidation propagates through isGameProper() -// which checks the SuperGame's registry for blacklist/retirement status. -``` - -### Remaining (Deployment) - -Deploy superchain-level infrastructure: -1. Deploy a SystemConfig with zero/minimal values for superchain use -2. Deploy AnchorStateRegistry initialized with that SystemConfig -3. Deploy DisputeGameFactory (SuperDisputeGameFactory) -4. Register SuperFaultDisputeGame implementation - ---- - -## Invalidation Propagation - -### How It Works - -When Guardian blacklists or retires a SuperGame in the superchain AnchorStateRegistry: - -1. SuperGame is marked as blacklisted/retired in superchain AnchorStateRegistry -2. Per-chain AnchorStateRegistry.isGameProper(delegatedGame) is called -3. The try/catch detects it's a DelegatedDisputeGame -4. It queries the SuperGame's registry (superchain AnchorStateRegistry) -5. If SuperGame is blacklisted/retired, returns false -6. Portal's withdrawal validation fails - -### What Gets Checked - -| Check | Where | Scope | -|-------|-------|-------| -| DelegatedGame blacklisted | Per-chain registry | Single game | -| DelegatedGame retired | Per-chain registry | Games before timestamp | -| SuperGame blacklisted | Superchain registry | Propagates to all DelegatedGames | -| SuperGame retired | Superchain registry | Propagates to all DelegatedGames | -| Global pause | SuperchainConfig | All games everywhere | - ---- - -## Files Modified - -| File | Change Type | Description | -|------|-------------|-------------| -| `interfaces/dispute/IDelegatedDisputeGame.sol` | **NEW** | Minimal interface for SuperGame detection | -| `src/dispute/AnchorStateRegistry.sol` | Modified | Added SuperGame validity check in isGameProper() | -| `src/dispute/DelegatedDisputeGame.sol` | Modified | Removed registry mismatch check | - ---- - -## Testing Strategy - -### Unit Tests - -1. isGameProper() correctly detects DelegatedDisputeGame and checks SuperGame -2. Non-DelegatedDisputeGame passes through isGameProper() normally - -### Integration Tests - -1. SuperGame blacklist propagates to DelegatedDisputeGames -2. SuperGame retirement propagates to DelegatedDisputeGames -3. Per-chain blacklist works independently of SuperGame status -4. Full withdrawal flow with new architecture - ---- - -## Summary - -The key insight is that we don't need any special interface changes. The superchain level just deploys a normal SystemConfig (with zero/minimal values) and a standard AnchorStateRegistry. The only code change needed was adding the try/catch in `isGameProper()` to propagate SuperGame invalidation to DelegatedDisputeGames. From 75624244daf91a09784a7dc51ff5e0c4fe0786a5 Mon Sep 17 00:00:00 2001 From: opsuperchain Date: Sun, 21 Dec 2025 04:19:31 +0000 Subject: [PATCH 5/9] fix(contracts): Verify SuperGame is registered in superchain factory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add isGameRegistered() check for the SuperGame in isGameProper(). This prevents fake SuperGames from being used - the SuperGame must be created by the legitimate superchain DisputeGameFactory. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../contracts-bedrock/src/dispute/AnchorStateRegistry.sol | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol b/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol index 3eb29b740aa..ed5c5b6ec0c 100644 --- a/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol +++ b/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol @@ -285,10 +285,16 @@ contract AnchorStateRegistry is ProxyAdminOwnedBase, Initializable, Reinitializa } // For DelegatedDisputeGames, also check SuperGame validity. - // If the SuperGame is blacklisted or retired, the DelegatedDisputeGame is also invalid. + // If the SuperGame is not registered, blacklisted, or retired, the DelegatedDisputeGame is also invalid. try IDelegatedDisputeGame(address(_game)).superGame() returns (ISuperFaultDisputeGame superGame) { IAnchorStateRegistry superRegistry = superGame.anchorStateRegistry(); + // SuperGame must be registered in the superchain DisputeGameFactory. + // This prevents fake SuperGames from being used. + if (!superRegistry.isGameRegistered(IDisputeGame(address(superGame)))) { + return false; + } + // SuperGame must not be blacklisted. if (superRegistry.isGameBlacklisted(IDisputeGame(address(superGame)))) { return false; From 8201c72cd6e51fcac129af6bda522002a920ed48 Mon Sep 17 00:00:00 2001 From: opsuperchain Date: Sun, 21 Dec 2025 04:30:40 +0000 Subject: [PATCH 6/9] feat(contracts): Add trusted superchain registry to DelegatedDisputeGame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add SUPERCHAIN_REGISTRY immutable to DelegatedDisputeGame that holds a trusted reference to the superchain-level AnchorStateRegistry. This is used in initialize() to verify the SuperGame is actually registered in the legitimate superchain factory, preventing fake SuperGames. Changes: - Add SUPERCHAIN_REGISTRY immutable to DelegatedDisputeGame - Add SuperGameNotRegistered error - Check isGameRegistered on superchain registry in initialize() - Add superchainRegistry() getter - Update IDelegatedDisputeGame interface πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../dispute/IDelegatedDisputeGame.sol | 4 +++ .../src/dispute/DelegatedDisputeGame.sol | 31 ++++++++++++++----- .../test/dispute/DelegatedDisputeGame.t.sol | 9 ++++-- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/dispute/IDelegatedDisputeGame.sol b/packages/contracts-bedrock/interfaces/dispute/IDelegatedDisputeGame.sol index 7b5390f459b..0eeb6195501 100644 --- a/packages/contracts-bedrock/interfaces/dispute/IDelegatedDisputeGame.sol +++ b/packages/contracts-bedrock/interfaces/dispute/IDelegatedDisputeGame.sol @@ -20,4 +20,8 @@ interface IDelegatedDisputeGame is IDisputeGame { /// @notice Returns the anchor state registry. /// @return registry_ The anchor state registry. function anchorStateRegistry() external view returns (IAnchorStateRegistry registry_); + + /// @notice Returns the superchain-level anchor state registry. + /// @return registry_ The superchain anchor state registry. + function superchainRegistry() external view returns (IAnchorStateRegistry registry_); } diff --git a/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol b/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol index 723be218597..5da20095b15 100644 --- a/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol @@ -122,9 +122,12 @@ contract DelegatedDisputeGame is Clone, IDisputeGame { /// @notice The game type ID for this delegated dispute game. GameType internal immutable GAME_TYPE; - /// @notice The anchor state registry for validation. + /// @notice The anchor state registry for this chain's validation. IAnchorStateRegistry internal immutable ANCHOR_STATE_REGISTRY; + /// @notice The superchain-level anchor state registry for SuperGame validation. + IAnchorStateRegistry internal immutable SUPERCHAIN_REGISTRY; + //////////////////////////////////////////////////////////////// // MUTABLE STATE // //////////////////////////////////////////////////////////////// @@ -166,15 +169,20 @@ contract DelegatedDisputeGame is Clone, IDisputeGame { /// @notice Thrown when the SuperGame address is zero. error InvalidSuperGame(); + /// @notice Thrown when the SuperGame is not registered in the superchain registry. + error SuperGameNotRegistered(); + //////////////////////////////////////////////////////////////// // CONSTRUCTOR // //////////////////////////////////////////////////////////////// /// @param _gameType The game type ID for this delegated dispute game. - /// @param _anchorStateRegistry The anchor state registry for validation. - constructor(GameType _gameType, IAnchorStateRegistry _anchorStateRegistry) { + /// @param _anchorStateRegistry The anchor state registry for this chain. + /// @param _superchainRegistry The superchain-level anchor state registry for SuperGame validation. + constructor(GameType _gameType, IAnchorStateRegistry _anchorStateRegistry, IAnchorStateRegistry _superchainRegistry) { GAME_TYPE = _gameType; ANCHOR_STATE_REGISTRY = _anchorStateRegistry; + SUPERCHAIN_REGISTRY = _superchainRegistry; } //////////////////////////////////////////////////////////////// @@ -200,15 +208,16 @@ contract DelegatedDisputeGame is Clone, IDisputeGame { // INVARIANT: SuperGame address must not be zero. if (address(superGameContract) == address(0)) revert InvalidSuperGame(); + // INVARIANT: SuperGame must be registered in the superchain registry. + // This prevents fake SuperGames from being used. + if (!SUPERCHAIN_REGISTRY.isGameRegistered(IDisputeGame(address(superGameContract)))) { + revert SuperGameNotRegistered(); + } + // Verify the root claim matches what the SuperGame has for this chain. Claim expectedRoot = superGameContract.rootClaimByChainId(chainId()); if (rootClaim().raw() != expectedRoot.raw()) revert RootClaimMismatch(); - // Note: We intentionally do NOT check that the SuperGame uses the same AnchorStateRegistry. - // The DelegatedDisputeGame may use a per-chain AnchorStateRegistry while the SuperGame uses - // a superchain-level AnchorStateRegistry. Invalidation propagates through isGameProper() - // which checks the SuperGame's registry for blacklist/retirement status. - // Verify the block number matches the output root proof and header RLP. _verifyBlockNumber(); @@ -308,6 +317,12 @@ contract DelegatedDisputeGame is Clone, IDisputeGame { registry_ = ANCHOR_STATE_REGISTRY; } + /// @notice Returns the superchain-level anchor state registry. + /// @return registry_ The superchain anchor state registry. + function superchainRegistry() public view returns (IAnchorStateRegistry registry_) { + registry_ = SUPERCHAIN_REGISTRY; + } + /// @notice Returns the super game this delegated game is linked to. /// @return superGame_ The super fault dispute game. function superGame() public pure returns (ISuperFaultDisputeGame superGame_) { diff --git a/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol index 9b854eddc0a..b48c2d166f6 100644 --- a/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol @@ -84,7 +84,8 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { init({ _rootClaim: ROOT_CLAIM, _absolutePrestate: absolutePrestate, _super: SUPER_ROOT_PROOF }); // Deploy the DelegatedDisputeGame implementation with constructor args. - delegatedGameImpl = new DelegatedDisputeGame(DELEGATED_GAME_TYPE, anchorStateRegistry); + // In tests, we use the same registry for both per-chain and superchain since we're testing mechanics. + delegatedGameImpl = new DelegatedDisputeGame(DELEGATED_GAME_TYPE, anchorStateRegistry, anchorStateRegistry); // Register the implementation with the factory. // Transfer ownership to this test contract first (already done in parent setUp). @@ -541,7 +542,8 @@ contract DelegatedDisputeGame_TestInit is BaseSuperFaultDisputeGame_TestInit { init({ _rootClaim: ROOT_CLAIM, _absolutePrestate: absolutePrestate, _super: SUPER_ROOT_PROOF }); // Deploy the DelegatedDisputeGame implementation with constructor args. - delegatedGameImpl = new DelegatedDisputeGame(DELEGATED_GAME_TYPE, anchorStateRegistry); + // In tests, we use the same registry for both per-chain and superchain since we're testing mechanics. + delegatedGameImpl = new DelegatedDisputeGame(DELEGATED_GAME_TYPE, anchorStateRegistry, anchorStateRegistry); // Register the implementation with the factory. disputeGameFactory.setImplementation(DELEGATED_GAME_TYPE, delegatedGameImpl); @@ -723,7 +725,8 @@ contract DelegatedDisputeGame_AnchorRegistry_Test is DelegatedDisputeGame_TestIn /// @notice Tests that isGameRegistered returns false for an unregistered game. function test_isGameRegistered_unregisteredGame_fails() public { // Create a game directly (not through factory) - it won't be registered. - DelegatedDisputeGame unregisteredGame = new DelegatedDisputeGame(DELEGATED_GAME_TYPE, anchorStateRegistry); + DelegatedDisputeGame unregisteredGame = + new DelegatedDisputeGame(DELEGATED_GAME_TYPE, anchorStateRegistry, anchorStateRegistry); assertFalse(anchorStateRegistry.isGameRegistered(unregisteredGame)); } From b1fd866cc211092ab9e451d83c630d47a07691ec Mon Sep 17 00:00:00 2001 From: opsuperchain Date: Sun, 21 Dec 2025 04:38:39 +0000 Subject: [PATCH 7/9] fix(contracts): Verify SuperGame's registry matches SUPERCHAIN_REGISTRY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add check in initialize() to ensure the SuperGame's anchorStateRegistry() matches the SUPERCHAIN_REGISTRY passed to constructor. This ensures consistency between validation in initialize() and isGameProper(). Without this check, an attacker could deploy a DelegatedDisputeGame with a fake SUPERCHAIN_REGISTRY that validates any SuperGame, while the real SuperGame uses a different registry. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/dispute/DelegatedDisputeGame.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol b/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol index 5da20095b15..282fad82eb1 100644 --- a/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol @@ -172,6 +172,9 @@ contract DelegatedDisputeGame is Clone, IDisputeGame { /// @notice Thrown when the SuperGame is not registered in the superchain registry. error SuperGameNotRegistered(); + /// @notice Thrown when the SuperGame's registry doesn't match SUPERCHAIN_REGISTRY. + error SuperchainRegistryMismatch(); + //////////////////////////////////////////////////////////////// // CONSTRUCTOR // //////////////////////////////////////////////////////////////// @@ -214,6 +217,12 @@ contract DelegatedDisputeGame is Clone, IDisputeGame { revert SuperGameNotRegistered(); } + // INVARIANT: SuperGame's registry must match SUPERCHAIN_REGISTRY. + // This ensures consistency between initialize() and isGameProper() validation. + if (address(superGameContract.anchorStateRegistry()) != address(SUPERCHAIN_REGISTRY)) { + revert SuperchainRegistryMismatch(); + } + // Verify the root claim matches what the SuperGame has for this chain. Claim expectedRoot = superGameContract.rootClaimByChainId(chainId()); if (rootClaim().raw() != expectedRoot.raw()) revert RootClaimMismatch(); From d9568ba4e96cc8ca058183753bca77b2e725e7b3 Mon Sep 17 00:00:00 2001 From: opsuperchain Date: Sun, 21 Dec 2025 05:01:55 +0000 Subject: [PATCH 8/9] feat(DelegatedDisputeGame): add chain ID validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add validation that the chain ID in extraData matches the per-chain factory's L2 chain ID from SystemConfig. This prevents creating delegated games for the wrong chain on a given factory. Also updates tests to: - Mock l2ChainId() to return 5 for testing - Use existing delegatedGameProxy instead of creating duplicates - Set respected game type in setUp for AnchorRegistry tests - Simplify tests that previously tried to create multiple games πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/dispute/DelegatedDisputeGame.sol | 8 + .../test/dispute/DelegatedDisputeGame.t.sol | 172 +++++++++--------- 2 files changed, 96 insertions(+), 84 deletions(-) diff --git a/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol b/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol index 282fad82eb1..fca4de52155 100644 --- a/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/DelegatedDisputeGame.sol @@ -175,6 +175,9 @@ contract DelegatedDisputeGame is Clone, IDisputeGame { /// @notice Thrown when the SuperGame's registry doesn't match SUPERCHAIN_REGISTRY. error SuperchainRegistryMismatch(); + /// @notice Thrown when the chain ID in extraData doesn't match the actual chain. + error InvalidChainId(); + //////////////////////////////////////////////////////////////// // CONSTRUCTOR // //////////////////////////////////////////////////////////////// @@ -202,6 +205,11 @@ contract DelegatedDisputeGame is Clone, IDisputeGame { // INVARIANT: No bonds are accepted for delegated games. if (msg.value != 0) revert NoBondsAccepted(); + // INVARIANT: Chain ID must match this factory's L2 chain. + // The per-chain AnchorStateRegistry's SystemConfig knows the correct L2 chain ID. + // This prevents creating games for other chains on this factory. + if (chainId() != ANCHOR_STATE_REGISTRY.systemConfig().l2ChainId()) revert InvalidChainId(); + // Mark as initialized before external calls (CEI pattern). initialized = true; diff --git a/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol index b48c2d166f6..9898377b17b 100644 --- a/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol @@ -16,6 +16,7 @@ import { DelegatedDisputeGame } from "src/dispute/DelegatedDisputeGame.sol"; // Interfaces import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { ISuperFaultDisputeGame } from "interfaces/dispute/ISuperFaultDisputeGame.sol"; +import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; /// @title DelegatedDisputeGame_Test /// @notice Tests for the DelegatedDisputeGame contract using standard DisputeGameFactory. @@ -93,6 +94,14 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { // Set init bond to 0 for delegated games (no bonds). disputeGameFactory.setInitBond(DELEGATED_GAME_TYPE, 0); + + // Mock the SystemConfig.l2ChainId() to return chainId 5 for testing. + // In production, each per-chain factory would have its own SystemConfig with the correct L2 chain ID. + vm.mockCall( + address(anchorStateRegistry.systemConfig()), + abi.encodeWithSelector(ISystemConfig.l2ChainId.selector), + abi.encode(5) + ); } /// @notice Helper to change the VM status byte of a claim. @@ -354,13 +363,9 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { function test_gameCount_increments() public { uint256 gameCountBefore = disputeGameFactory.gameCount(); - // Create first delegated game for chain 5. + // Create delegated game for chain 5. _createDelegatedGameChain5(5000); assertEq(disputeGameFactory.gameCount(), gameCountBefore + 1); - - // Create second delegated game for chain 6. - _createDelegatedGameChain6(6000); - assertEq(disputeGameFactory.gameCount(), gameCountBefore + 2); } /// @notice Tests that createdAt is set correctly. @@ -396,29 +401,33 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { assertTrue(delegatedGameProxy.l1Head().raw() != bytes32(0)); } - /// @notice Tests creating multiple delegated games for different chains from same SuperGame. - function test_multipleChains_succeeds() public { + /// @notice Tests creating multiple delegated games for same chain from same SuperGame. + /// @dev Note: Each per-chain factory can only create games for its L2 chain. + /// Multi-chain testing would require separate factories with different l2ChainId configs. + function test_delegatedGame_correctConfiguration_succeeds() public { // Create delegated game for chain 5 - DelegatedDisputeGame game5 = _createDelegatedGameChain5(5000); + DelegatedDisputeGame game = _createDelegatedGameChain5(5000); - // Create delegated game for chain 6 - DelegatedDisputeGame game6 = _createDelegatedGameChain6(6000); + // Verify game points to correct SuperGame + assertEq(address(game.superGame()), address(gameProxy)); - // Verify both games point to same SuperGame - assertEq(address(game5.superGame()), address(gameProxy)); - assertEq(address(game6.superGame()), address(gameProxy)); + // Verify correct chain ID + assertEq(game.chainId(), 5); - // Verify different chain IDs - assertEq(game5.chainId(), 5); - assertEq(game6.chainId(), 6); + // Verify status delegates to SuperGame + assertEq(uint256(game.status()), uint256(gameProxy.status())); - // Verify different root claims - assertEq(game5.rootClaim().raw(), outputRootChain5); - assertEq(game6.rootClaim().raw(), outputRootChain6); + // Verify L2 block number is correct + assertEq(game.l2SequenceNumber(), 5000); + } + + /// @notice Tests that creating a game with wrong chain ID reverts. + function test_create_wrongChainId_reverts() public { + // Try to create a game for chain 6 on this factory (which is configured for chain 5). + bytes memory extraData = _createExtendedExtraData(6000, address(gameProxy), 6, proofChain6, headerRLPChain6); - // Verify both delegate status to same SuperGame - assertEq(uint256(game5.status()), uint256(gameProxy.status())); - assertEq(uint256(game6.status()), uint256(gameProxy.status())); + vm.expectRevert(DelegatedDisputeGame.InvalidChainId.selector); + disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain6), extraData); } /// @notice Tests that create reverts with invalid output root proof. @@ -551,6 +560,19 @@ contract DelegatedDisputeGame_TestInit is BaseSuperFaultDisputeGame_TestInit { // Set init bond to 0 for delegated games (no bonds). disputeGameFactory.setInitBond(DELEGATED_GAME_TYPE, 0); + // Mock the SystemConfig.l2ChainId() to return chainId 5 for testing. + // In production, each per-chain factory would have its own SystemConfig with the correct L2 chain ID. + vm.mockCall( + address(anchorStateRegistry.systemConfig()), + abi.encodeWithSelector(ISystemConfig.l2ChainId.selector), + abi.encode(5) + ); + + // Set the delegated game type as the respected type before creating the game. + // This ensures wasRespectedGameTypeWhenCreated is true for tests that need it. + vm.prank(anchorStateRegistry.superchainConfig().guardian()); + anchorStateRegistry.setRespectedGameType(DELEGATED_GAME_TYPE); + // Create a delegated game for chain 5 with the correct block number. delegatedGameProxy = _createDelegatedGameChain5(delegatedL2BlockNumber); } @@ -740,53 +762,41 @@ contract DelegatedDisputeGame_AnchorRegistry_Test is DelegatedDisputeGame_TestIn } /// @notice Tests that isGameProper returns true when game type is respected. - function test_isGameProper_whenRespected_succeeds() public { - // Set the delegated game type as the respected type. - vm.prank(anchorStateRegistry.superchainConfig().guardian()); - anchorStateRegistry.setRespectedGameType(DELEGATED_GAME_TYPE); - - // Create a new delegated game for chain 6 (chain 5 already used in setUp). - DelegatedDisputeGame newGame = _createDelegatedGameChain6(6000); - - assertTrue(anchorStateRegistry.isGameProper(newGame)); + function test_isGameProper_whenRespected_succeeds() public view { + // The delegated game was created when respected (set in setUp). + assertTrue(anchorStateRegistry.isGameProper(delegatedGameProxy)); } /// @notice Tests that isGameProper returns false when game is blacklisted. function test_isGameProper_blacklisted_fails() public { - // Set the delegated game type as respected first. - vm.prank(anchorStateRegistry.superchainConfig().guardian()); - anchorStateRegistry.setRespectedGameType(DELEGATED_GAME_TYPE); - - // Create a new game for chain 6 when respected (chain 5 already used in setUp). - DelegatedDisputeGame newGame = _createDelegatedGameChain6(6000); - // Verify it's proper before blacklisting. - assertTrue(anchorStateRegistry.isGameProper(newGame)); + assertTrue(anchorStateRegistry.isGameProper(delegatedGameProxy)); // Blacklist the game. vm.prank(anchorStateRegistry.superchainConfig().guardian()); - anchorStateRegistry.blacklistDisputeGame(newGame); + anchorStateRegistry.blacklistDisputeGame(delegatedGameProxy); // Now it should not be proper. - assertFalse(anchorStateRegistry.isGameProper(newGame)); + assertFalse(anchorStateRegistry.isGameProper(delegatedGameProxy)); } /// @notice Tests that isGameRespected returns the correct value. - function test_isGameRespected_whenRespected_succeeds() public { - // Set the delegated game type as the respected type. - vm.prank(anchorStateRegistry.superchainConfig().guardian()); - anchorStateRegistry.setRespectedGameType(DELEGATED_GAME_TYPE); + function test_isGameRespected_whenRespected_succeeds() public view { + // The delegated game was created when respected (set in setUp). + assertTrue(anchorStateRegistry.isGameRespected(delegatedGameProxy)); + } - // Create a new delegated game for chain 6 (chain 5 already used in setUp). - DelegatedDisputeGame newGame = _createDelegatedGameChain6(6000); + /// @notice Tests that changing respected game type after creation doesn't affect existing games. + function test_isGameRespected_unchangedAfterRespectedTypeChange() public { + // Game was created when respected. + assertTrue(anchorStateRegistry.isGameRespected(delegatedGameProxy)); - assertTrue(anchorStateRegistry.isGameRespected(newGame)); - } + // Change respected game type to something else. + vm.prank(anchorStateRegistry.superchainConfig().guardian()); + anchorStateRegistry.setRespectedGameType(GameType.wrap(999)); - /// @notice Tests that isGameRespected returns false when not respected. - function test_isGameRespected_whenNotRespected_fails() public view { - // The delegated game was created when its type was not respected. - assertFalse(anchorStateRegistry.isGameRespected(delegatedGameProxy)); + // Game should still be respected (wasRespectedGameTypeWhenCreated is immutable). + assertTrue(anchorStateRegistry.isGameRespected(delegatedGameProxy)); } /// @notice Tests that isGameResolved returns correct value based on SuperGame status. @@ -900,24 +910,19 @@ contract DelegatedDisputeGame_Resolution_Test is DelegatedDisputeGame_TestInit { assertEq(uint256(status), uint256(GameStatus.CHALLENGER_WINS)); } - /// @notice Tests multiple delegated games share resolution from same SuperGame. - function test_multipleGames_sharedResolution() public { - // Create another delegated game for chain 6. - DelegatedDisputeGame game6 = _createDelegatedGameChain6(6000); - - // Both should be IN_PROGRESS. + /// @notice Tests delegated game shares resolution with SuperGame. + function test_sharedResolution_succeeds() public { + // Delegated game should be IN_PROGRESS. assertEq(uint256(delegatedGameProxy.status()), uint256(GameStatus.IN_PROGRESS)); - assertEq(uint256(game6.status()), uint256(GameStatus.IN_PROGRESS)); // Resolve the SuperGame. _resolveSuperGame(); - // Both should now be DEFENDER_WINS. + // Delegated game should now be DEFENDER_WINS. assertEq(uint256(delegatedGameProxy.status()), uint256(GameStatus.DEFENDER_WINS)); - assertEq(uint256(game6.status()), uint256(GameStatus.DEFENDER_WINS)); - // Both should have same resolvedAt. - assertEq(delegatedGameProxy.resolvedAt().raw(), game6.resolvedAt().raw()); + // Delegated game should have same resolvedAt as SuperGame. + assertEq(delegatedGameProxy.resolvedAt().raw(), gameProxy.resolvedAt().raw()); } } @@ -925,16 +930,15 @@ contract DelegatedDisputeGame_Resolution_Test is DelegatedDisputeGame_TestInit { /// @notice Tests for DelegatedDisputeGame edge cases. contract DelegatedDisputeGame_EdgeCases_Test is DelegatedDisputeGame_TestInit { /// @notice Tests that a delegated game can be created after SuperGame is resolved. - function test_create_afterSuperGameResolved_succeeds() public { - // Resolve the SuperGame first. - _resolveSuperGame(); + function test_status_afterSuperGameResolved_succeeds() public { + // Verify status is IN_PROGRESS before resolution. + assertEq(uint256(delegatedGameProxy.status()), uint256(GameStatus.IN_PROGRESS)); - // Create a new delegated game for chain 6 (chain 5 already used in setUp). - DelegatedDisputeGame newGame = _createDelegatedGameChain6(6000); + // Resolve the SuperGame. + _resolveSuperGame(); - // Verify the game was created and status is DEFENDER_WINS (delegates to resolved SuperGame). - assertTrue(address(newGame) != address(0)); - assertEq(uint256(newGame.status()), uint256(GameStatus.DEFENDER_WINS)); + // Verify status is now DEFENDER_WINS (delegates to resolved SuperGame). + assertEq(uint256(delegatedGameProxy.status()), uint256(GameStatus.DEFENDER_WINS)); } /// @notice Tests that initialize reverts if called twice. @@ -944,36 +948,36 @@ contract DelegatedDisputeGame_EdgeCases_Test is DelegatedDisputeGame_TestInit { delegatedGameProxy.initialize(); } - /// @notice Tests that creation fails with wrong chain ID (root claim mismatch). - function test_create_mismatchedChainId_reverts() public { - // Get root claim for chain 5 but try to use chain 6's extraData structure. - // This creates extraData pointing to chain 6 but using chain 5's proof. + /// @notice Tests that creation fails with wrong root claim. + function test_create_mismatchedRootClaim_reverts() public { + // Use chain 5's extraData but with a wrong root claim. bytes memory extraData = _createExtendedExtraData( delegatedL2BlockNumber, address(gameProxy), - 6, // Wrong chainId - should be 5 to match proofChain5 + 5, proofChain5, headerRLPChain5 ); - // Should revert because rootClaim (for chain 5's output root) doesn't match what SuperGame has for chain 6. + // Should revert because the provided rootClaim doesn't match what SuperGame has for chain 5. + bytes32 wrongRootClaim = bytes32(uint256(0xdead)); vm.expectRevert(DelegatedDisputeGame.RootClaimMismatch.selector); - disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); + disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(wrongRootClaim), extraData); } /// @notice Tests that SuperGame address zero is rejected. function test_create_invalidSuperGame_reverts() public { // Create extraData with address(0) as superGame. bytes memory extraData = _createExtendedExtraData( - 6000, + 5000, address(0), // Invalid zero address - 6, - proofChain6, - headerRLPChain6 + 5, + proofChain5, + headerRLPChain5 ); vm.expectRevert(DelegatedDisputeGame.InvalidSuperGame.selector); - disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain6), extraData); + disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); } /// @notice Tests that resolve can be called multiple times after resolution. From a3f280bff5e0dd7f92bfee4f148ff7c8b618c494 Mon Sep 17 00:00:00 2001 From: opsuperchain Date: Sun, 21 Dec 2025 05:17:00 +0000 Subject: [PATCH 9/9] refactor(tests): use actual l2ChainId instead of mocking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove vm.mockCall for l2ChainId and instead use the actual chain ID (901) from the deploy config. This makes tests more realistic by using actual contract values rather than mocks. Changes: - Replace chain 5/6 with chain 901/902 throughout tests - Remove ISystemConfig import (no longer needed) - Update all proof generation to use 901/902 storage roots - Update helper functions and assertions to use correct chain IDs πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../test/dispute/DelegatedDisputeGame.t.sol | 226 ++++++++---------- 1 file changed, 105 insertions(+), 121 deletions(-) diff --git a/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol index 9898377b17b..4bda89ffe4b 100644 --- a/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/DelegatedDisputeGame.t.sol @@ -16,7 +16,6 @@ import { DelegatedDisputeGame } from "src/dispute/DelegatedDisputeGame.sol"; // Interfaces import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { ISuperFaultDisputeGame } from "interfaces/dispute/ISuperFaultDisputeGame.sol"; -import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; /// @title DelegatedDisputeGame_Test /// @notice Tests for the DelegatedDisputeGame contract using standard DisputeGameFactory. @@ -31,15 +30,15 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { /// @dev A created DelegatedDisputeGame proxy. DelegatedDisputeGame internal delegatedGameProxy; - /// @dev Stored proof data for chain 5. - Types.OutputRootProof internal proofChain5; - bytes internal headerRLPChain5; - bytes32 internal outputRootChain5; + /// @dev Stored proof data for chain 901 (the actual l2ChainId from deploy config). + Types.OutputRootProof internal proofChain901; + bytes internal headerRLPChain901; + bytes32 internal outputRootChain901; - /// @dev Stored proof data for chain 6. - Types.OutputRootProof internal proofChain6; - bytes internal headerRLPChain6; - bytes32 internal outputRootChain6; + /// @dev Stored proof data for chain 902 (a second chain for testing). + Types.OutputRootProof internal proofChain902; + bytes internal headerRLPChain902; + bytes32 internal outputRootChain902; /// @dev The root claim of the game. Claim internal ROOT_CLAIM; @@ -67,18 +66,19 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { validl2SequenceNumber = l2Seqno + 1; // Generate real OutputRootProofs for each chain. - // These will be the actual root claims stored in the SuperGame. - (proofChain5, outputRootChain5, headerRLPChain5) = - _generateOutputRootProof(bytes32(uint256(5)), bytes32(uint256(5000)), abi.encodePacked(uint256(5000))); + // Chain 901 is the actual l2ChainId from deploy-config/hardhat.json. + // Chain 902 is a second chain for multi-chain testing. + (proofChain901, outputRootChain901, headerRLPChain901) = + _generateOutputRootProof(bytes32(uint256(901)), bytes32(uint256(5000)), abi.encodePacked(uint256(5000))); - (proofChain6, outputRootChain6, headerRLPChain6) = - _generateOutputRootProof(bytes32(uint256(6)), bytes32(uint256(6000)), abi.encodePacked(uint256(6000))); + (proofChain902, outputRootChain902, headerRLPChain902) = + _generateOutputRootProof(bytes32(uint256(902)), bytes32(uint256(6000)), abi.encodePacked(uint256(6000))); // Build SUPER_ROOT_PROOF with real OutputRootProof hashes. SUPER_ROOT_PROOF.version = bytes1(uint8(1)); SUPER_ROOT_PROOF.timestamp = uint64(validl2SequenceNumber); - SUPER_ROOT_PROOF.outputRoots.push(Types.OutputRootWithChainId({ chainId: 5, root: outputRootChain5 })); - SUPER_ROOT_PROOF.outputRoots.push(Types.OutputRootWithChainId({ chainId: 6, root: outputRootChain6 })); + SUPER_ROOT_PROOF.outputRoots.push(Types.OutputRootWithChainId({ chainId: 901, root: outputRootChain901 })); + SUPER_ROOT_PROOF.outputRoots.push(Types.OutputRootWithChainId({ chainId: 902, root: outputRootChain902 })); ROOT_CLAIM = Claim.wrap(Hashing.hashSuperRootProof(SUPER_ROOT_PROOF)); // Initialize the SuperGame with real output roots. @@ -94,14 +94,6 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { // Set init bond to 0 for delegated games (no bonds). disputeGameFactory.setInitBond(DELEGATED_GAME_TYPE, 0); - - // Mock the SystemConfig.l2ChainId() to return chainId 5 for testing. - // In production, each per-chain factory would have its own SystemConfig with the correct L2 chain ID. - vm.mockCall( - address(anchorStateRegistry.systemConfig()), - abi.encodeWithSelector(ISystemConfig.l2ChainId.selector), - abi.encode(5) - ); } /// @notice Helper to change the VM status byte of a claim. @@ -190,46 +182,46 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { ); } - /// @notice Helper to create a delegated game for chain 5 (uses stored proof data). + /// @notice Helper to create a delegated game for chain 901 (uses stored proof data). /// @param _l2BlockNumber The L2 block number to claim. /// @return game_ The created delegated game. - function _createDelegatedGameChain5(uint256 _l2BlockNumber) + function _createDelegatedGameChain901(uint256 _l2BlockNumber) internal returns (DelegatedDisputeGame game_) { - // For chain 5, the block number in the proof is 5000. + // For chain 901, the block number in the proof is 5000. // If _l2BlockNumber matches 5000, it will pass verification. // Otherwise it will fail with L2BlockNumberMismatch. bytes memory extraData = _createExtendedExtraData( _l2BlockNumber, address(gameProxy), - 5, - proofChain5, - headerRLPChain5 + 901, + proofChain901, + headerRLPChain901 ); game_ = DelegatedDisputeGame( - payable(address(disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData))) + payable(address(disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain901), extraData))) ); } - /// @notice Helper to create a delegated game for chain 6 (uses stored proof data). + /// @notice Helper to create a delegated game for chain 902 (uses stored proof data). /// @param _l2BlockNumber The L2 block number to claim. /// @return game_ The created delegated game. - function _createDelegatedGameChain6(uint256 _l2BlockNumber) + function _createDelegatedGameChain902(uint256 _l2BlockNumber) internal returns (DelegatedDisputeGame game_) { bytes memory extraData = _createExtendedExtraData( _l2BlockNumber, address(gameProxy), - 6, - proofChain6, - headerRLPChain6 + 902, + proofChain902, + headerRLPChain902 ); game_ = DelegatedDisputeGame( - payable(address(disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain6), extraData))) + payable(address(disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain902), extraData))) ); } @@ -241,16 +233,16 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { /// @notice Tests that a delegated game can be created successfully via standard factory. function test_create_succeeds() public { - // Create delegated game for chain 5 with matching block number (5000). - delegatedGameProxy = _createDelegatedGameChain5(5000); + // Create delegated game for chain 901 with matching block number (5000). + delegatedGameProxy = _createDelegatedGameChain901(5000); // Verify the game was created. assertTrue(address(delegatedGameProxy) != address(0)); // Verify the game's properties. assertEq(delegatedGameProxy.gameType().raw(), DELEGATED_GAME_TYPE.raw()); - assertEq(delegatedGameProxy.rootClaim().raw(), outputRootChain5); - assertEq(delegatedGameProxy.chainId(), 5); + assertEq(delegatedGameProxy.rootClaim().raw(), outputRootChain901); + assertEq(delegatedGameProxy.chainId(), 901); assertEq(address(delegatedGameProxy.superGame()), address(gameProxy)); assertEq(address(delegatedGameProxy.anchorStateRegistry()), address(anchorStateRegistry)); @@ -268,14 +260,14 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { /// @notice Tests that create reverts if root claim doesn't match SuperGame. function test_create_rootClaimMismatch_reverts() public { - uint256 chainId = 5; + uint256 chainId = 901; uint256 l2BlockNumber = 5000; // Use a different root claim than what SuperGame returns. Claim wrongRootClaim = Claim.wrap(bytes32(uint256(12345))); bytes memory extraData = _createExtendedExtraData( - l2BlockNumber, address(gameProxy), chainId, proofChain5, headerRLPChain5 + l2BlockNumber, address(gameProxy), chainId, proofChain901, headerRLPChain901 ); vm.expectRevert(DelegatedDisputeGame.RootClaimMismatch.selector); @@ -286,42 +278,42 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { /// @dev The factory rejects with IncorrectBondAmount since init bond is 0. function test_create_withBonds_reverts() public { bytes memory extraData = _createExtendedExtraData( - 5000, address(gameProxy), 5, proofChain5, headerRLPChain5 + 5000, address(gameProxy), 901, proofChain901, headerRLPChain901 ); // Factory rejects because init bond is 0, so any value is incorrect vm.expectRevert(abi.encodeWithSignature("IncorrectBondAmount()")); - disputeGameFactory.create{ value: 1 ether }(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); + disputeGameFactory.create{ value: 1 ether }(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain901), extraData); } /// @notice Tests that status is delegated to SuperGame. function test_status_delegatesToSuperGame() public { - delegatedGameProxy = _createDelegatedGameChain5(5000); + delegatedGameProxy = _createDelegatedGameChain901(5000); // Status should match SuperGame's status. assertEq(uint256(delegatedGameProxy.status()), uint256(gameProxy.status())); } /// @notice Tests that resolvedAt is delegated to SuperGame. function test_resolvedAt_delegatesToSuperGame() public { - delegatedGameProxy = _createDelegatedGameChain5(5000); + delegatedGameProxy = _createDelegatedGameChain901(5000); // resolvedAt should match SuperGame's resolvedAt. assertEq(delegatedGameProxy.resolvedAt().raw(), gameProxy.resolvedAt().raw()); } /// @notice Tests that l2SequenceNumber returns the block number from extraData. function test_l2SequenceNumber_returnsBlockNumber() public { - delegatedGameProxy = _createDelegatedGameChain5(5000); + delegatedGameProxy = _createDelegatedGameChain901(5000); assertEq(delegatedGameProxy.l2SequenceNumber(), 5000); } /// @notice Tests that gameData returns correct values. function test_gameData_returnsCorrectValues() public { - delegatedGameProxy = _createDelegatedGameChain5(5000); + delegatedGameProxy = _createDelegatedGameChain901(5000); (GameType gameType_, Claim rootClaim_, bytes memory extraData_) = delegatedGameProxy.gameData(); assertEq(gameType_.raw(), DELEGATED_GAME_TYPE.raw()); - assertEq(rootClaim_.raw(), outputRootChain5); + assertEq(rootClaim_.raw(), outputRootChain901); // extraData first 32 bytes are l2BlockNumber (use l2SequenceNumber for viem compatibility) uint256 decodedBlockNumber; assembly { @@ -334,7 +326,7 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { function test_gameAtIndex_succeeds() public { uint256 gameCountBefore = disputeGameFactory.gameCount(); - DelegatedDisputeGame proxy = _createDelegatedGameChain5(5000); + DelegatedDisputeGame proxy = _createDelegatedGameChain901(5000); (GameType gameType_, Timestamp timestamp_, IDisputeGame proxy_) = disputeGameFactory.gameAtIndex(gameCountBefore); @@ -346,14 +338,14 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { /// @notice Tests that games() lookup works correctly. function test_games_succeeds() public { bytes memory extraData = _createExtendedExtraData( - 5000, address(gameProxy), 5, proofChain5, headerRLPChain5 + 5000, address(gameProxy), 901, proofChain901, headerRLPChain901 ); - IDisputeGame proxy = disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); + IDisputeGame proxy = disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain901), extraData); // Look up by game parameters (IDisputeGame proxy_, Timestamp timestamp_) = - disputeGameFactory.games(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); + disputeGameFactory.games(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain901), extraData); assertTrue(timestamp_.raw() > 0); assertEq(address(proxy_), address(proxy)); @@ -363,22 +355,22 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { function test_gameCount_increments() public { uint256 gameCountBefore = disputeGameFactory.gameCount(); - // Create delegated game for chain 5. - _createDelegatedGameChain5(5000); + // Create delegated game for chain 901. + _createDelegatedGameChain901(5000); assertEq(disputeGameFactory.gameCount(), gameCountBefore + 1); } /// @notice Tests that createdAt is set correctly. function test_createdAt_isSetCorrectly() public { uint256 expectedTimestamp = block.timestamp; - delegatedGameProxy = _createDelegatedGameChain5(5000); + delegatedGameProxy = _createDelegatedGameChain901(5000); assertEq(delegatedGameProxy.createdAt().raw(), expectedTimestamp); } /// @notice Tests that wasRespectedGameTypeWhenCreated returns correct value. function test_wasRespectedGameTypeWhenCreated_returnsCorrectValue() public { // The delegated game type is not the respected type (SUPER_CANNON is), so this should be false. - delegatedGameProxy = _createDelegatedGameChain5(5000); + delegatedGameProxy = _createDelegatedGameChain901(5000); assertFalse(delegatedGameProxy.wasRespectedGameTypeWhenCreated()); } @@ -389,14 +381,14 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { /// @notice Tests that gameCreator returns the correct address. function test_gameCreator_returnsCorrectAddress() public { - delegatedGameProxy = _createDelegatedGameChain5(5000); + delegatedGameProxy = _createDelegatedGameChain901(5000); // gameCreator should be the caller of create() (this test contract) assertEq(delegatedGameProxy.gameCreator(), address(this)); } /// @notice Tests that l1Head returns a non-zero value. function test_l1Head_returnsNonZero() public { - delegatedGameProxy = _createDelegatedGameChain5(5000); + delegatedGameProxy = _createDelegatedGameChain901(5000); // l1Head should be the parent block hash assertTrue(delegatedGameProxy.l1Head().raw() != bytes32(0)); } @@ -405,14 +397,14 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { /// @dev Note: Each per-chain factory can only create games for its L2 chain. /// Multi-chain testing would require separate factories with different l2ChainId configs. function test_delegatedGame_correctConfiguration_succeeds() public { - // Create delegated game for chain 5 - DelegatedDisputeGame game = _createDelegatedGameChain5(5000); + // Create delegated game for chain 901 + DelegatedDisputeGame game = _createDelegatedGameChain901(5000); // Verify game points to correct SuperGame assertEq(address(game.superGame()), address(gameProxy)); // Verify correct chain ID - assertEq(game.chainId(), 5); + assertEq(game.chainId(), 901); // Verify status delegates to SuperGame assertEq(uint256(game.status()), uint256(gameProxy.status())); @@ -423,25 +415,25 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { /// @notice Tests that creating a game with wrong chain ID reverts. function test_create_wrongChainId_reverts() public { - // Try to create a game for chain 6 on this factory (which is configured for chain 5). - bytes memory extraData = _createExtendedExtraData(6000, address(gameProxy), 6, proofChain6, headerRLPChain6); + // Try to create a game for chain 902 on this factory (which is configured for chain 901). + bytes memory extraData = _createExtendedExtraData(6000, address(gameProxy), 902, proofChain902, headerRLPChain902); vm.expectRevert(DelegatedDisputeGame.InvalidChainId.selector); - disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain6), extraData); + disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain902), extraData); } /// @notice Tests that create reverts with invalid output root proof. function test_create_invalidOutputRootProof_reverts() public { // Create a corrupted proof (different stateRoot than what was used). - Types.OutputRootProof memory corruptedProof = proofChain5; + Types.OutputRootProof memory corruptedProof = proofChain901; corruptedProof.stateRoot = bytes32(uint256(999)); bytes memory extraData = _createExtendedExtraData( - 5000, address(gameProxy), 5, corruptedProof, headerRLPChain5 + 5000, address(gameProxy), 901, corruptedProof, headerRLPChain901 ); vm.expectRevert(DelegatedDisputeGame.InvalidOutputRootProof.selector); - disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); + disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain901), extraData); } /// @notice Tests that create reverts with invalid header RLP. @@ -450,11 +442,11 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { bytes memory invalidHeaderRLP = hex"DEADBEEF"; bytes memory extraData = _createExtendedExtraData( - 5000, address(gameProxy), 5, proofChain5, invalidHeaderRLP + 5000, address(gameProxy), 901, proofChain901, invalidHeaderRLP ); vm.expectRevert(DelegatedDisputeGame.InvalidHeaderRLP.selector); - disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); + disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain901), extraData); } /// @notice Tests that create reverts with mismatched block number. @@ -463,11 +455,11 @@ contract DelegatedDisputeGame_Test is BaseSuperFaultDisputeGame_TestInit { uint256 wrongBlockNumber = 2000; bytes memory extraData = _createExtendedExtraData( - wrongBlockNumber, address(gameProxy), 5, proofChain5, headerRLPChain5 + wrongBlockNumber, address(gameProxy), 901, proofChain901, headerRLPChain901 ); vm.expectRevert(DelegatedDisputeGame.L2BlockNumberMismatch.selector); - disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); + disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain901), extraData); } } @@ -490,15 +482,15 @@ contract DelegatedDisputeGame_TestInit is BaseSuperFaultDisputeGame_TestInit { /// @dev The L2 block number for the delegated game (encoded in the proof). uint256 internal delegatedL2BlockNumber; - /// @dev Stored proof data for chain 5. - Types.OutputRootProof internal proofChain5; - bytes internal headerRLPChain5; - bytes32 internal outputRootChain5; + /// @dev Stored proof data for chain 901 (the actual l2ChainId from deploy config). + Types.OutputRootProof internal proofChain901; + bytes internal headerRLPChain901; + bytes32 internal outputRootChain901; - /// @dev Stored proof data for chain 6. - Types.OutputRootProof internal proofChain6; - bytes internal headerRLPChain6; - bytes32 internal outputRootChain6; + /// @dev Stored proof data for chain 902 (a second chain for testing). + Types.OutputRootProof internal proofChain902; + bytes internal headerRLPChain902; + bytes32 internal outputRootChain902; /// @dev The root claim of the game. Claim internal ROOT_CLAIM; @@ -526,25 +518,25 @@ contract DelegatedDisputeGame_TestInit is BaseSuperFaultDisputeGame_TestInit { validl2SequenceNumber = l2Seqno + 1; // Generate real OutputRootProofs for each chain. - // Chain 5 has block number 5000, chain 6 has block number 6000. - delegatedChainId = 5; + // Chain 901 has block number 5000, chain 902 has block number 6000. + delegatedChainId = 901; delegatedL2BlockNumber = 5000; - (proofChain5, outputRootChain5, headerRLPChain5) = + (proofChain901, outputRootChain901, headerRLPChain901) = _generateOutputRootProof( - bytes32(uint256(5)), + bytes32(uint256(901)), bytes32(uint256(delegatedL2BlockNumber)), abi.encodePacked(delegatedL2BlockNumber) ); - (proofChain6, outputRootChain6, headerRLPChain6) = - _generateOutputRootProof(bytes32(uint256(6)), bytes32(uint256(6000)), abi.encodePacked(uint256(6000))); + (proofChain902, outputRootChain902, headerRLPChain902) = + _generateOutputRootProof(bytes32(uint256(902)), bytes32(uint256(6000)), abi.encodePacked(uint256(6000))); // Build SUPER_ROOT_PROOF with real OutputRootProof hashes. SUPER_ROOT_PROOF.version = bytes1(uint8(1)); SUPER_ROOT_PROOF.timestamp = uint64(validl2SequenceNumber); - SUPER_ROOT_PROOF.outputRoots.push(Types.OutputRootWithChainId({ chainId: 5, root: outputRootChain5 })); - SUPER_ROOT_PROOF.outputRoots.push(Types.OutputRootWithChainId({ chainId: 6, root: outputRootChain6 })); + SUPER_ROOT_PROOF.outputRoots.push(Types.OutputRootWithChainId({ chainId: 901, root: outputRootChain901 })); + SUPER_ROOT_PROOF.outputRoots.push(Types.OutputRootWithChainId({ chainId: 902, root: outputRootChain902 })); ROOT_CLAIM = Claim.wrap(Hashing.hashSuperRootProof(SUPER_ROOT_PROOF)); // Initialize the SuperGame with real output roots. @@ -560,21 +552,13 @@ contract DelegatedDisputeGame_TestInit is BaseSuperFaultDisputeGame_TestInit { // Set init bond to 0 for delegated games (no bonds). disputeGameFactory.setInitBond(DELEGATED_GAME_TYPE, 0); - // Mock the SystemConfig.l2ChainId() to return chainId 5 for testing. - // In production, each per-chain factory would have its own SystemConfig with the correct L2 chain ID. - vm.mockCall( - address(anchorStateRegistry.systemConfig()), - abi.encodeWithSelector(ISystemConfig.l2ChainId.selector), - abi.encode(5) - ); - // Set the delegated game type as the respected type before creating the game. // This ensures wasRespectedGameTypeWhenCreated is true for tests that need it. vm.prank(anchorStateRegistry.superchainConfig().guardian()); anchorStateRegistry.setRespectedGameType(DELEGATED_GAME_TYPE); - // Create a delegated game for chain 5 with the correct block number. - delegatedGameProxy = _createDelegatedGameChain5(delegatedL2BlockNumber); + // Create a delegated game for chain 901 with the correct block number. + delegatedGameProxy = _createDelegatedGameChain901(delegatedL2BlockNumber); } /// @notice Helper to change the VM status byte of a claim. @@ -641,39 +625,39 @@ contract DelegatedDisputeGame_TestInit is BaseSuperFaultDisputeGame_TestInit { ); } - /// @notice Helper to create a delegated game for chain 5 (uses stored proof data). - function _createDelegatedGameChain5(uint256 _l2BlockNumber) + /// @notice Helper to create a delegated game for chain 901 (uses stored proof data). + function _createDelegatedGameChain901(uint256 _l2BlockNumber) internal returns (DelegatedDisputeGame game_) { bytes memory extraData = _createExtendedExtraData( _l2BlockNumber, address(gameProxy), - 5, - proofChain5, - headerRLPChain5 + 901, + proofChain901, + headerRLPChain901 ); game_ = DelegatedDisputeGame( - payable(address(disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData))) + payable(address(disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain901), extraData))) ); } - /// @notice Helper to create a delegated game for chain 6 (uses stored proof data). - function _createDelegatedGameChain6(uint256 _l2BlockNumber) + /// @notice Helper to create a delegated game for chain 902 (uses stored proof data). + function _createDelegatedGameChain902(uint256 _l2BlockNumber) internal returns (DelegatedDisputeGame game_) { bytes memory extraData = _createExtendedExtraData( _l2BlockNumber, address(gameProxy), - 6, - proofChain6, - headerRLPChain6 + 902, + proofChain902, + headerRLPChain902 ); game_ = DelegatedDisputeGame( - payable(address(disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain6), extraData))) + payable(address(disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain902), extraData))) ); } @@ -706,7 +690,7 @@ contract DelegatedDisputeGame_TestInit is BaseSuperFaultDisputeGame_TestInit { gameCounter++; // Get the expected block number for this chain. - uint256 blockNumber = _chainId == 5 ? delegatedL2BlockNumber : 6000; + uint256 blockNumber = _chainId == 901 ? delegatedL2BlockNumber : 6000; // Generate a unique proof by varying the storage root. (Types.OutputRootProof memory proof, bytes32 outputRoot, bytes memory headerRLP) = @@ -950,16 +934,16 @@ contract DelegatedDisputeGame_EdgeCases_Test is DelegatedDisputeGame_TestInit { /// @notice Tests that creation fails with wrong root claim. function test_create_mismatchedRootClaim_reverts() public { - // Use chain 5's extraData but with a wrong root claim. + // Use chain 901's extraData but with a wrong root claim. bytes memory extraData = _createExtendedExtraData( delegatedL2BlockNumber, address(gameProxy), - 5, - proofChain5, - headerRLPChain5 + 901, + proofChain901, + headerRLPChain901 ); - // Should revert because the provided rootClaim doesn't match what SuperGame has for chain 5. + // Should revert because the provided rootClaim doesn't match what SuperGame has for chain 901. bytes32 wrongRootClaim = bytes32(uint256(0xdead)); vm.expectRevert(DelegatedDisputeGame.RootClaimMismatch.selector); disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(wrongRootClaim), extraData); @@ -971,13 +955,13 @@ contract DelegatedDisputeGame_EdgeCases_Test is DelegatedDisputeGame_TestInit { bytes memory extraData = _createExtendedExtraData( 5000, address(0), // Invalid zero address - 5, - proofChain5, - headerRLPChain5 + 901, + proofChain901, + headerRLPChain901 ); vm.expectRevert(DelegatedDisputeGame.InvalidSuperGame.selector); - disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain5), extraData); + disputeGameFactory.create(DELEGATED_GAME_TYPE, Claim.wrap(outputRootChain901), extraData); } /// @notice Tests that resolve can be called multiple times after resolution.