// SPDX-License-Identifier: MIT pragma solidity 0.8.15; // Contracts import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; import { ProxyAdminOwnedBase } from "src/L1/ProxyAdminOwnedBase.sol"; // Libraries import { LibClone } from "@solady/utils/LibClone.sol"; import { GameType, Claim, GameId, Timestamp, Hash, LibGameId } from "src/dispute/lib/Types.sol"; import { NoImplementation, IncorrectBondAmount, GameAlreadyExists } from "src/dispute/lib/Errors.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; /// @custom:proxied true /// @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 ProxyAdminOwnedBase, ReinitializableBase, OwnableUpgradeable, ISemver { /// @dev Allows for the creation of clone proxies with immutable arguments. using LibClone for address; /// @notice Emitted when a new dispute game is created /// @param disputeProxy The address of the dispute game proxy /// @param gameType The type of the dispute game proxy's implementation /// @param rootClaim The root claim of the dispute game event DisputeGameCreated(address indexed disputeProxy, GameType indexed gameType, Claim indexed rootClaim); /// @notice Emitted when a new game implementation added to the factory /// @param impl The implementation contract for the given `GameType`. /// @param gameType The type of the DisputeGame. event ImplementationSet(address indexed impl, GameType indexed gameType); /// @notice Emitted when a game type's implementation args are set /// @param gameType The type of the DisputeGame. /// @param args The constructor args for the game type. event ImplementationArgsSet(GameType indexed gameType, bytes args); /// @notice Emitted when a game type's initialization bond is updated /// @param gameType The type of the DisputeGame. /// @param newBond The new bond (in wei) for initializing the game type. event InitBondUpdated(GameType indexed gameType, uint256 indexed newBond); /// @notice Information about a dispute game found in a `findLatestGames` search. struct GameSearchResult { uint256 index; GameId metadata; Timestamp timestamp; Claim rootClaim; bytes extraData; } /// @notice Semantic version. /// @custom:semver 1.4.0 string public constant version = "1.4.0"; /// @notice `gameImpls` is a mapping that maps `GameType`s to their respective /// `IDisputeGame` implementations. mapping(GameType => IDisputeGame) public gameImpls; /// @notice Returns the required bonds for initializing a dispute game of the given type. 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 Maps each Game Type to an associated configuration to use with it, but because we need to pass them /// to a clone with immutable args so they have to be stored as arbitrary bytes unfortunately mapping(GameType => bytes) public gameArgs; /// @notice Constructs a new DisputeGameFactory contract. constructor() OwnableUpgradeable() ReinitializableBase(1) { _disableInitializers(); } /// @notice Initializes the contract. /// @param _owner The owner of the contract. function initialize(address _owner) external reinitializer(initVersion()) { // Initialization transactions must come from the ProxyAdmin or its owner. _assertOnlyProxyAdminOrProxyAdminOwner(); // Now perform initialization logic. __Ownable_init(); _transferOwnership(_owner); } /// @notice The total number of dispute games created by this factory. /// @return gameCount_ The total number of dispute games created by this factory. function gameCount() external view returns (uint256 gameCount_) { gameCount_ = _disputeGameList.length; } /// @notice `games` queries an internal mapping that maps the hash of /// `gameType ++ rootClaim ++ extraData` to the deployed `DisputeGame` clone. /// @dev `++` equates to concatenation. /// @param _gameType The type of the DisputeGame - used to decide the proxy implementation /// @param _rootClaim The root claim of the DisputeGame. /// @param _extraData Any extra data that should be provided to the created dispute game. /// @return proxy_ The clone of the `DisputeGame` created with the given parameters. /// Returns `address(0)` if nonexistent. /// @return timestamp_ The timestamp of the creation of the dispute game. 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); } /// @notice `gameAtIndex` returns the dispute game contract address and its creation timestamp /// at the given index. Each created dispute game increments the underlying index. /// Reverts if the provided index does not correspond to an existing dispute game. /// @param _index The index of the dispute game. /// @return gameType_ The type of the DisputeGame - used to decide the proxy implementation. /// @return timestamp_ The timestamp of the creation of the dispute game. /// @return proxy_ The clone of the `DisputeGame` created with the given parameters. 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)); } function create( GameType _gameType, Claim _rootClaim, bytes calldata _extraData ) external payable returns (IDisputeGame proxy_) { proxy_ = _createGameImpl(_gameType, _rootClaim, _extraData); proxy_.initialize{ value: msg.value }(); _finalizeGameCreation(_gameType, _rootClaim, _extraData, proxy_); } function createWithInitData( GameType _gameType, Claim _rootClaim, bytes calldata _extraData, bytes calldata _initData ) external payable returns (IDisputeGame proxy_) { proxy_ = _createGameImpl(_gameType, _rootClaim, _extraData); proxy_.initializeWithInitData{ value: msg.value }(_initData); _finalizeGameCreation(_gameType, _rootClaim, _extraData, proxy_); } /// @notice Creates a new DisputeGame proxy contract. /// @param _gameType The type of the DisputeGame - used to decide the proxy implementation. /// @param _rootClaim The root claim of the DisputeGame. /// @param _extraData Any extra data that should be provided to the created dispute game. /// @param proxy_ The address of the created DisputeGame proxy. function _finalizeGameCreation( GameType _gameType, Claim _rootClaim, bytes calldata _extraData, IDisputeGame proxy_ ) internal { // 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); emit DisputeGameCreated(address(proxy_), _gameType, _rootClaim); } function _createGameImpl( GameType _gameType, Claim _rootClaim, bytes calldata _extraData ) internal 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); // 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); // Cache gameArgs to avoid stack-too-deep errors bytes memory implArgs = gameArgs[_gameType]; if (implArgs.length == 0) { // 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, parentHash, _extraData))); } else { // 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, 88) │ Game type │ // │ [88, 88 + n) │ Extra data (opaque) │ // │ [88 + n, 88 + n + m) │ Implementation args (opaque) │ // └──────────────────────┴─────────────────────────────────────┘ proxy_ = IDisputeGame( address(impl) .clone(abi.encodePacked(msg.sender, _rootClaim, parentHash, _gameType, _extraData, implArgs)) ); } } /// @notice Returns a unique identifier for the given dispute game parameters. /// @dev Hashes the concatenation of `gameType . rootClaim . extraData` /// without expanding memory. /// @param _gameType The type of the DisputeGame. /// @param _rootClaim The root claim of the DisputeGame. /// @param _extraData Any extra data that should be provided to the created dispute game. /// @return uuid_ The unique identifier for the given dispute game parameters. function getGameUUID( GameType _gameType, Claim _rootClaim, bytes calldata _extraData ) public pure returns (Hash uuid_) { uuid_ = Hash.wrap(keccak256(abi.encode(_gameType, _rootClaim, _extraData))); } /// @notice Finds the `_n` most recent `GameId`'s of type `_gameType` starting at `_start`. If there are less than /// `_n` games of type `_gameType` starting at `_start`, then the returned array will be shorter than `_n`. /// @param _gameType The type of game to find. /// @param _start The index to start the reverse search from. /// @param _n The number of games to find. 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--; } } } /// @notice Sets the implementation contract for a specific `GameType`. /// @dev May only be called by the `owner`. /// @param _gameType The type of the DisputeGame. /// @param _impl The implementation contract for the given `GameType`. function setImplementation(GameType _gameType, IDisputeGame _impl) external onlyOwner { gameImpls[_gameType] = _impl; emit ImplementationSet(address(_impl), _gameType); } /// @notice Sets the implementation contract for a specific `GameType`. /// @dev May only be called by the `owner`. /// @param _gameType The type of the DisputeGame. /// @param _impl The implementation contract for the given `GameType`. /// @param _args The constructor args to be passed for each implementation function setImplementation(GameType _gameType, IDisputeGame _impl, bytes calldata _args) external onlyOwner { gameImpls[_gameType] = _impl; gameArgs[_gameType] = _args; emit ImplementationSet(address(_impl), _gameType); emit ImplementationArgsSet(_gameType, _args); } /// @notice Sets the bond (in wei) for initializing a game type. /// @dev May only be called by the `owner`. /// @param _gameType The type of the DisputeGame. /// @param _initBond The bond (in wei) for initializing a game type. function setInitBond(GameType _gameType, uint256 _initBond) external onlyOwner { initBonds[_gameType] = _initBond; emit InitBondUpdated(_gameType, _initBond); } }