github.com/ethereum-optimism/optimism@v1.7.2/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol (about)

     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.15;
     3  
     4  import "src/libraries/DisputeTypes.sol";
     5  import "src/libraries/DisputeErrors.sol";
     6  
     7  import { Test } from "forge-std/Test.sol";
     8  import { DisputeGameFactory, IDisputeGameFactory } from "src/dispute/DisputeGameFactory.sol";
     9  import { IDisputeGame } from "src/dispute/interfaces/IDisputeGame.sol";
    10  import { Proxy } from "src/universal/Proxy.sol";
    11  import { CommonTest } from "test/setup/CommonTest.sol";
    12  
    13  contract DisputeGameFactory_Init is CommonTest {
    14      FakeClone fakeClone;
    15  
    16      event DisputeGameCreated(address indexed disputeProxy, GameType indexed gameType, Claim indexed rootClaim);
    17      event ImplementationSet(address indexed impl, GameType indexed gameType);
    18      event InitBondUpdated(GameType indexed gameType, uint256 indexed newBond);
    19  
    20      function setUp() public virtual override {
    21          super.enableFaultProofs();
    22          super.setUp();
    23          fakeClone = new FakeClone();
    24  
    25          // Transfer ownership of the factory to the test contract.
    26          vm.prank(deploy.mustGetAddress("SystemOwnerSafe"));
    27          disputeGameFactory.transferOwnership(address(this));
    28      }
    29  }
    30  
    31  contract DisputeGameFactory_Create_Test is DisputeGameFactory_Init {
    32      /// @dev Tests that the `create` function succeeds when creating a new dispute game
    33      ///      with a `GameType` that has an implementation set.
    34      function testFuzz_create_succeeds(
    35          uint8 gameType,
    36          Claim rootClaim,
    37          bytes calldata extraData,
    38          uint256 _value
    39      )
    40          public
    41      {
    42          // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values.
    43          GameType gt = GameType.wrap(uint8(bound(gameType, 0, 2)));
    44          // Ensure the rootClaim has a VMStatus that disagrees with the validity.
    45          rootClaim = changeClaimStatus(rootClaim, VMStatuses.INVALID);
    46  
    47          // Set all three implementations to the same `FakeClone` contract.
    48          for (uint8 i; i < 3; i++) {
    49              GameType lgt = GameType.wrap(i);
    50              disputeGameFactory.setImplementation(lgt, IDisputeGame(address(fakeClone)));
    51              disputeGameFactory.setInitBond(lgt, _value);
    52          }
    53  
    54          vm.deal(address(this), _value);
    55  
    56          vm.expectEmit(false, true, true, false);
    57          emit DisputeGameCreated(address(0), gt, rootClaim);
    58          IDisputeGame proxy = disputeGameFactory.create{ value: _value }(gt, rootClaim, extraData);
    59  
    60          (IDisputeGame game, Timestamp timestamp) = disputeGameFactory.games(gt, rootClaim, extraData);
    61  
    62          // Ensure that the dispute game was assigned to the `disputeGames` mapping.
    63          assertEq(address(game), address(proxy));
    64          assertEq(Timestamp.unwrap(timestamp), block.timestamp);
    65          assertEq(disputeGameFactory.gameCount(), 1);
    66  
    67          (, Timestamp timestamp2, IDisputeGame game2) = disputeGameFactory.gameAtIndex(0);
    68          assertEq(address(game2), address(proxy));
    69          assertEq(Timestamp.unwrap(timestamp2), block.timestamp);
    70  
    71          // Ensure that the game proxy received the bonded ETH.
    72          assertEq(address(proxy).balance, _value);
    73      }
    74  
    75      /// @dev Tests that the `create` function reverts when creating a new dispute game with an insufficient bond.
    76      function testFuzz_create_insufficientBond_reverts(
    77          uint8 gameType,
    78          Claim rootClaim,
    79          bytes calldata extraData
    80      )
    81          public
    82      {
    83          // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values.
    84          GameType gt = GameType.wrap(uint8(bound(gameType, 0, 2)));
    85          // Ensure the rootClaim has a VMStatus that disagrees with the validity.
    86          rootClaim = changeClaimStatus(rootClaim, VMStatuses.INVALID);
    87  
    88          // Set all three implementations to the same `FakeClone` contract.
    89          for (uint8 i; i < 3; i++) {
    90              GameType lgt = GameType.wrap(i);
    91              disputeGameFactory.setImplementation(lgt, IDisputeGame(address(fakeClone)));
    92              disputeGameFactory.setInitBond(lgt, 1 ether);
    93          }
    94  
    95          vm.expectRevert(InsufficientBond.selector);
    96          disputeGameFactory.create(gt, rootClaim, extraData);
    97      }
    98  
    99      /// @dev Tests that the `create` function reverts when there is no implementation
   100      ///      set for the given `GameType`.
   101      function testFuzz_create_noImpl_reverts(uint32 gameType, Claim rootClaim, bytes calldata extraData) public {
   102          // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values. We skip over
   103          // game type = 0, since the deploy script set the implementation for that game type.
   104          GameType gt = GameType.wrap(uint32(bound(gameType, 2, type(uint32).max)));
   105          // Ensure the rootClaim has a VMStatus that disagrees with the validity.
   106          rootClaim = changeClaimStatus(rootClaim, VMStatuses.INVALID);
   107  
   108          vm.expectRevert(abi.encodeWithSelector(NoImplementation.selector, gt));
   109          disputeGameFactory.create(gt, rootClaim, extraData);
   110      }
   111  
   112      /// @dev Tests that the `create` function reverts when there exists a dispute game with the same UUID.
   113      function testFuzz_create_sameUUID_reverts(uint8 gameType, Claim rootClaim, bytes calldata extraData) public {
   114          // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values.
   115          GameType gt = GameType.wrap(uint8(bound(gameType, 0, 2)));
   116          // Ensure the rootClaim has a VMStatus that disagrees with the validity.
   117          rootClaim = changeClaimStatus(rootClaim, VMStatuses.INVALID);
   118  
   119          // Set all three implementations to the same `FakeClone` contract.
   120          for (uint8 i; i < 3; i++) {
   121              disputeGameFactory.setImplementation(GameType.wrap(i), IDisputeGame(address(fakeClone)));
   122          }
   123  
   124          // Create our first dispute game - this should succeed.
   125          vm.expectEmit(false, true, true, false);
   126          emit DisputeGameCreated(address(0), gt, rootClaim);
   127          IDisputeGame proxy = disputeGameFactory.create(gt, rootClaim, extraData);
   128  
   129          (IDisputeGame game, Timestamp timestamp) = disputeGameFactory.games(gt, rootClaim, extraData);
   130          // Ensure that the dispute game was assigned to the `disputeGames` mapping.
   131          assertEq(address(game), address(proxy));
   132          assertEq(Timestamp.unwrap(timestamp), block.timestamp);
   133  
   134          // Ensure that the `create` function reverts when called with parameters that would result in the same UUID.
   135          vm.expectRevert(
   136              abi.encodeWithSelector(GameAlreadyExists.selector, disputeGameFactory.getGameUUID(gt, rootClaim, extraData))
   137          );
   138          disputeGameFactory.create(gt, rootClaim, extraData);
   139      }
   140  
   141      function changeClaimStatus(Claim _claim, VMStatus _status) public pure returns (Claim out_) {
   142          assembly {
   143              out_ := or(and(not(shl(248, 0xFF)), _claim), shl(248, _status))
   144          }
   145      }
   146  }
   147  
   148  contract DisputeGameFactory_SetImplementation_Test is DisputeGameFactory_Init {
   149      /// @dev Tests that the `setImplementation` function properly sets the implementation for a given `GameType`.
   150      function test_setImplementation_succeeds() public {
   151          vm.expectEmit(true, true, true, true, address(disputeGameFactory));
   152          emit ImplementationSet(address(1), GameTypes.CANNON);
   153  
   154          // Set the implementation for the `GameTypes.CANNON` enum value.
   155          disputeGameFactory.setImplementation(GameTypes.CANNON, IDisputeGame(address(1)));
   156  
   157          // Ensure that the implementation for the `GameTypes.CANNON` enum value is set.
   158          assertEq(address(disputeGameFactory.gameImpls(GameTypes.CANNON)), address(1));
   159      }
   160  
   161      /// @dev Tests that the `setImplementation` function reverts when called by a non-owner.
   162      function test_setImplementation_notOwner_reverts() public {
   163          // Ensure that the `setImplementation` function reverts when called by a non-owner.
   164          vm.prank(address(0));
   165          vm.expectRevert("Ownable: caller is not the owner");
   166          disputeGameFactory.setImplementation(GameTypes.CANNON, IDisputeGame(address(1)));
   167      }
   168  }
   169  
   170  contract DisputeGameFactory_SetInitBond_Test is DisputeGameFactory_Init {
   171      /// @dev Tests that the `setInitBond` function properly sets the init bond for a given `GameType`.
   172      function test_setInitBond_succeeds() public {
   173          // There should be no init bond for the `GameTypes.CANNON` enum value, it has not been set.
   174          assertEq(disputeGameFactory.initBonds(GameTypes.CANNON), 0);
   175  
   176          vm.expectEmit(true, true, true, true, address(disputeGameFactory));
   177          emit InitBondUpdated(GameTypes.CANNON, 1 ether);
   178  
   179          // Set the init bond for the `GameTypes.CANNON` enum value.
   180          disputeGameFactory.setInitBond(GameTypes.CANNON, 1 ether);
   181  
   182          // Ensure that the init bond for the `GameTypes.CANNON` enum value is set.
   183          assertEq(disputeGameFactory.initBonds(GameTypes.CANNON), 1 ether);
   184      }
   185  
   186      /// @dev Tests that the `setInitBond` function reverts when called by a non-owner.
   187      function test_setInitBond_notOwner_reverts() public {
   188          // Ensure that the `setInitBond` function reverts when called by a non-owner.
   189          vm.prank(address(0));
   190          vm.expectRevert("Ownable: caller is not the owner");
   191          disputeGameFactory.setInitBond(GameTypes.CANNON, 1 ether);
   192      }
   193  }
   194  
   195  contract DisputeGameFactory_GetGameUUID_Test is DisputeGameFactory_Init {
   196      /// @dev Tests that the `getGameUUID` function returns the correct hash when comparing
   197      ///      against the keccak256 hash of the abi-encoded parameters.
   198      function testDiff_getGameUUID_succeeds(uint8 gameType, Claim rootClaim, bytes calldata extraData) public {
   199          // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values.
   200          GameType gt = GameType.wrap(uint8(bound(gameType, 0, 2)));
   201  
   202          assertEq(
   203              Hash.unwrap(disputeGameFactory.getGameUUID(gt, rootClaim, extraData)),
   204              keccak256(abi.encode(gt, rootClaim, extraData))
   205          );
   206      }
   207  }
   208  
   209  contract DisputeGameFactory_Owner_Test is DisputeGameFactory_Init {
   210      /// @dev Tests that the `owner` function returns the correct address after deployment.
   211      function test_owner_succeeds() public {
   212          assertEq(disputeGameFactory.owner(), address(this));
   213      }
   214  }
   215  
   216  contract DisputeGameFactory_TransferOwnership_Test is DisputeGameFactory_Init {
   217      /// @dev Tests that the `transferOwnership` function succeeds when called by the owner.
   218      function test_transferOwnership_succeeds() public {
   219          disputeGameFactory.transferOwnership(address(1));
   220          assertEq(disputeGameFactory.owner(), address(1));
   221      }
   222  
   223      /// @dev Tests that the `transferOwnership` function reverts when called by a non-owner.
   224      function test_transferOwnership_notOwner_reverts() public {
   225          vm.prank(address(0));
   226          vm.expectRevert("Ownable: caller is not the owner");
   227          disputeGameFactory.transferOwnership(address(1));
   228      }
   229  }
   230  
   231  contract DisputeGameFactory_FindLatestGames_Test is DisputeGameFactory_Init {
   232      function setUp() public override {
   233          super.setUp();
   234  
   235          // Set three implementations to the same `FakeClone` contract.
   236          for (uint8 i; i < 3; i++) {
   237              GameType lgt = GameType.wrap(i);
   238              disputeGameFactory.setImplementation(lgt, IDisputeGame(address(fakeClone)));
   239              disputeGameFactory.setInitBond(lgt, 0);
   240          }
   241      }
   242  
   243      /// @dev Tests that `findLatestGames` returns an empty array when the passed starting index is greater than or equal
   244      ///      to the game count.
   245      function testFuzz_findLatestGames_greaterThanLength_succeeds(uint256 _start) public {
   246          // Create some dispute games of varying game types.
   247          for (uint256 i; i < 1 << 5; i++) {
   248              disputeGameFactory.create(GameType.wrap(uint8(i % 2)), Claim.wrap(bytes32(i)), abi.encode(i));
   249          }
   250  
   251          // Bound the starting index to a number greater than the length of the game list.
   252          uint256 gameCount = disputeGameFactory.gameCount();
   253          _start = bound(_start, gameCount, type(uint256).max);
   254  
   255          // The array's length should always be 0.
   256          IDisputeGameFactory.GameSearchResult[] memory games =
   257              disputeGameFactory.findLatestGames(GameTypes.CANNON, _start, 1);
   258          assertEq(games.length, 0);
   259      }
   260  
   261      /// @dev Tests that `findLatestGames` returns the correct games.
   262      function test_findLatestGames_static_succeeds() public {
   263          // Create some dispute games of varying game types.
   264          for (uint256 i; i < 1 << 5; i++) {
   265              disputeGameFactory.create(GameType.wrap(uint8(i % 3)), Claim.wrap(bytes32(i)), abi.encode(i));
   266          }
   267  
   268          uint256 gameCount = disputeGameFactory.gameCount();
   269  
   270          IDisputeGameFactory.GameSearchResult[] memory games;
   271  
   272          games = disputeGameFactory.findLatestGames(GameType.wrap(0), gameCount - 1, 1);
   273          assertEq(games.length, 1);
   274          assertEq(games[0].index, 30);
   275          (GameType gameType, Timestamp createdAt, IDisputeGame game) = games[0].metadata.unpack();
   276          assertEq(gameType.raw(), 0);
   277          assertEq(createdAt.raw(), block.timestamp);
   278  
   279          games = disputeGameFactory.findLatestGames(GameType.wrap(1), gameCount - 1, 1);
   280          assertEq(games.length, 1);
   281          assertEq(games[0].index, 31);
   282          (gameType, createdAt, game) = games[0].metadata.unpack();
   283          assertEq(gameType.raw(), 1);
   284          assertEq(createdAt.raw(), block.timestamp);
   285  
   286          games = disputeGameFactory.findLatestGames(GameType.wrap(2), gameCount - 1, 1);
   287          assertEq(games.length, 1);
   288          assertEq(games[0].index, 29);
   289          (gameType, createdAt, game) = games[0].metadata.unpack();
   290          assertEq(gameType.raw(), 2);
   291          assertEq(createdAt.raw(), block.timestamp);
   292      }
   293  
   294      /// @dev Tests that `findLatestGames` returns the correct games, if there are less than `_n` games of the given type
   295      ///      available.
   296      function test_findLatestGames_lessThanNAvailable_succeeds() public {
   297          // Create some dispute games of varying game types.
   298          disputeGameFactory.create(GameType.wrap(1), Claim.wrap(bytes32(0)), abi.encode(0));
   299          disputeGameFactory.create(GameType.wrap(1), Claim.wrap(bytes32(uint256(1))), abi.encode(1));
   300          for (uint256 i; i < 1 << 3; i++) {
   301              disputeGameFactory.create(GameType.wrap(0), Claim.wrap(bytes32(i)), abi.encode(i));
   302          }
   303  
   304          uint256 gameCount = disputeGameFactory.gameCount();
   305  
   306          IDisputeGameFactory.GameSearchResult[] memory games;
   307  
   308          games = disputeGameFactory.findLatestGames(GameType.wrap(2), gameCount - 1, 5);
   309          assertEq(games.length, 0);
   310  
   311          games = disputeGameFactory.findLatestGames(GameType.wrap(1), gameCount - 1, 5);
   312          assertEq(games.length, 2);
   313          assertEq(games[0].index, 1);
   314          assertEq(games[1].index, 0);
   315      }
   316  
   317      /// @dev Tests that the expected number of games are returned when `findLatestGames` is called.
   318      function testFuzz_findLatestGames_correctAmount_succeeds(
   319          uint256 _numGames,
   320          uint256 _numSearchedGames,
   321          uint256 _n
   322      )
   323          public
   324      {
   325          _numGames = bound(_numGames, 0, 1 << 8);
   326          _numSearchedGames = bound(_numSearchedGames, 0, _numGames);
   327          _n = bound(_n, 0, _numSearchedGames);
   328  
   329          // Create `_numGames` dispute games, with at least `_numSearchedGames` games.
   330          for (uint256 i; i < _numGames; i++) {
   331              uint8 gameType = i < _numSearchedGames ? 0 : 1;
   332              disputeGameFactory.create(GameType.wrap(gameType), Claim.wrap(bytes32(i)), abi.encode(i));
   333          }
   334  
   335          // Ensure that the correct number of games are returned.
   336          uint256 start = _numGames == 0 ? 0 : _numGames - 1;
   337          IDisputeGameFactory.GameSearchResult[] memory games =
   338              disputeGameFactory.findLatestGames(GameType.wrap(0), start, _n);
   339          assertEq(games.length, _n);
   340      }
   341  }
   342  
   343  /// @dev A fake clone used for testing the `DisputeGameFactory` contract's `create` function.
   344  contract FakeClone {
   345      function initialize() external payable {
   346          // noop
   347      }
   348  
   349      function extraData() external pure returns (bytes memory) {
   350          return hex"FF0420";
   351      }
   352  
   353      function parentHash() external pure returns (bytes32) {
   354          return bytes32(0);
   355      }
   356  
   357      function rootClaim() external pure returns (Claim) {
   358          return Claim.wrap(bytes32(0));
   359      }
   360  }