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 }