// SPDX-License-Identifier: MIT pragma solidity 0.8.15; import {ISemver} from "../../universal/ISemver.sol"; import {IDisputeGame} from "./interfaces/IDisputeGame.sol"; import {IDisputeGameFactory} from "./interfaces/IDisputeGameFactory.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {LibClone} from "solady/src/utils/LibClone.sol"; import "contracts/L1/dispute/lib/Types.sol"; import "contracts/L1/dispute/lib/Errors.sol"; /// @title DisputeGameFactory /// @notice A factory contract for creating `IDisputeGame` contracts. All created dispute games are stored in both a /// mapping and an append only array. The timestamp of the creation time of the dispute game is packed tightly /// into the storage slot with the address of the dispute game to make offchain discoverability of playable /// dispute games easier. contract DisputeGameFactory is OwnableUpgradeable, IDisputeGameFactory, ISemver { /// @dev Allows for the creation of clone proxies with immutable arguments. using LibClone for address; struct DisputeInfo { GameType gameType; address sender; uint256 bond; bytes32 l1Head; } /// @notice Semantic version. /// @custom:semver 1.0.0 string public constant version = "1.0.0"; /// @inheritdoc IDisputeGameFactory mapping(GameType => IDisputeGame) public gameImpls; /// @inheritdoc IDisputeGameFactory mapping(GameType => uint256) public initBonds; /// @notice Mapping of a hash of `gameType || rootClaim || extraData` to the deployed `IDisputeGame` clone (where // `||` denotes concatenation). mapping(Hash => GameId) internal _disputeGames; /// @notice An append-only array of disputeGames that have been created. Used by offchain game solvers to /// efficiently track dispute games. GameId[] internal _disputeGameList; /// @notice An array of dispute games that have been requested. mapping(bytes32 => DisputeInfo) public disputeGameCreationRequests; /// @notice Constructs a new DisputeGameFactory contract. constructor() OwnableUpgradeable() { initialize(address(0)); } /// @notice Initializes the contract. /// @param _owner The owner of the contract. function initialize(address _owner) public initializer { __Ownable_init(); _transferOwnership(_owner); } /// @inheritdoc IDisputeGameFactory function gameCount() external view returns (uint256 gameCount_) { gameCount_ = _disputeGameList.length; } /// @inheritdoc IDisputeGameFactory function games( GameType _gameType, Claim _rootClaim, bytes calldata _extraData ) external view returns (IDisputeGame proxy_, Timestamp timestamp_) { Hash uuid = getGameUUID(_gameType, _rootClaim, _extraData); (, Timestamp timestamp, address proxy) = _disputeGames[uuid].unpack(); (proxy_, timestamp_) = (IDisputeGame(proxy), timestamp); } /// @inheritdoc IDisputeGameFactory function gameAtIndex(uint256 _index) external view returns (GameType gameType_, Timestamp timestamp_, IDisputeGame proxy_) { (GameType gameType, Timestamp timestamp, address proxy) = _disputeGameList[_index].unpack(); (gameType_, timestamp_, proxy_) = (gameType, timestamp, IDisputeGame(proxy)); } /// @inheritdoc IDisputeGameFactory function dispute(GameType _gameType, bytes calldata _extraData) external payable { // Grab the implementation contract for the given `GameType`. IDisputeGame impl = gameImpls[_gameType]; // If there is no implementation to clone for the given `GameType`, revert. if (address(impl) == address(0)) revert NoImplementation(_gameType); // If the required initialization bond is not met, revert. if (msg.value != initBonds[_gameType]) revert IncorrectBondAmount(); // Get the hash of the parent block. bytes32 parentHash = blockhash(block.number - 1); // Save the dispute game creation request. DisputeInfo memory info = DisputeInfo({ gameType: _gameType, sender: msg.sender, bond: msg.value, l1Head: parentHash }); disputeGameCreationRequests[keccak256(abi.encodePacked(_gameType, _extraData))] = info; emit DisputeGameRequested(msg.sender, _gameType, msg.value, _extraData); } /// @inheritdoc IDisputeGameFactory function create( GameType _gameType, Claim _rootClaim, bytes calldata _extraData ) external onlyOwner returns (IDisputeGame proxy_) { // Grab the implementation contract for the given `GameType`. IDisputeGame impl = gameImpls[_gameType]; // If there is no implementation to clone for the given `GameType`, revert. if (address(impl) == address(0)) revert NoImplementation(_gameType); // find the given request bytes32 requestUuid = keccak256(abi.encodePacked(_gameType, _extraData)); DisputeInfo memory info = disputeGameCreationRequests[requestUuid]; if (info.l1Head == bytes32(0)) revert NoDisputeGameRequests(); // If the required initialization bond is not met, revert. if (info.bond != initBonds[_gameType]) revert IncorrectBondAmount(); // Clone the implementation contract and initialize it with the given parameters. // // CWIA Calldata Layout: // ┌──────────────┬────────────────────────────────────┐ // │ Bytes │ Description │ // ├──────────────┼────────────────────────────────────┤ // │ [0, 20) │ Game creator address │ // │ [20, 52) │ Root claim │ // │ [52, 84) │ Parent block hash at creation time │ // │ [84, 84 + n) │ Extra data (opaque) │ // └──────────────┴────────────────────────────────────┘ proxy_ = IDisputeGame(address(impl).clone(abi.encodePacked(msg.sender, _rootClaim, info.l1Head, _extraData))); proxy_.initialize{value: info.bond}(); // Compute the unique identifier for the dispute game. Hash uuid = getGameUUID(_gameType, _rootClaim, _extraData); // If a dispute game with the same UUID already exists, revert. if (GameId.unwrap(_disputeGames[uuid]) != bytes32(0)) revert GameAlreadyExists(uuid); // Pack the game ID. GameId id = LibGameId.pack(_gameType, Timestamp.wrap(uint64(block.timestamp)), address(proxy_)); // Store the dispute game id in the mapping & emit the `DisputeGameCreated` event. _disputeGames[uuid] = id; _disputeGameList.push(id); delete disputeGameCreationRequests[requestUuid]; emit DisputeGameCreated(address(proxy_), _gameType, _rootClaim); } /// @inheritdoc IDisputeGameFactory function getGameUUID( GameType _gameType, Claim _rootClaim, bytes calldata _extraData ) public pure returns (Hash uuid_) { uuid_ = Hash.wrap(keccak256(abi.encode(_gameType, _rootClaim, _extraData))); } /// @inheritdoc IDisputeGameFactory function findLatestGames( GameType _gameType, uint256 _start, uint256 _n ) external view returns (GameSearchResult[] memory games_) { // If the `_start` index is greater than or equal to the game array length or `_n == 0`, return an empty array. if (_start >= _disputeGameList.length || _n == 0) return games_; // Allocate enough memory for the full array, but start the array's length at `0`. We may not use all of the // memory allocated, but we don't know ahead of time the final size of the array. assembly { games_ := mload(0x40) mstore(0x40, add(games_, add(0x20, shl(0x05, _n)))) } // Perform a reverse linear search for the `_n` most recent games of type `_gameType`. for (uint256 i = _start; i >= 0 && i <= _start;) { GameId id = _disputeGameList[i]; (GameType gameType, Timestamp timestamp, address proxy) = id.unpack(); if (gameType.raw() == _gameType.raw()) { // Increase the size of the `games_` array by 1. // SAFETY: We can safely lazily allocate memory here because we pre-allocated enough memory for the max // possible size of the array. assembly { mstore(games_, add(mload(games_), 0x01)) } bytes memory extraData = IDisputeGame(proxy).extraData(); Claim rootClaim = IDisputeGame(proxy).rootClaim(); games_[games_.length - 1] = GameSearchResult({ index: i, metadata: id, timestamp: timestamp, rootClaim: rootClaim, extraData: extraData }); if (games_.length >= _n) break; } unchecked { i--; } } } /// @inheritdoc IDisputeGameFactory function setImplementation(GameType _gameType, IDisputeGame _impl) external onlyOwner { gameImpls[_gameType] = _impl; emit ImplementationSet(address(_impl), _gameType); } /// @inheritdoc IDisputeGameFactory function setInitBond(GameType _gameType, uint256 _initBond) external onlyOwner { initBonds[_gameType] = _initBond; emit InitBondUpdated(_gameType, _initBond); } }