diff --git a/op-acceptance-tests/tests/base/disputegame_v2/init_test.go b/op-acceptance-tests/tests/base/disputegame_v2/init_test.go index 7f14f8776a715..c777264489c80 100644 --- a/op-acceptance-tests/tests/base/disputegame_v2/init_test.go +++ b/op-acceptance-tests/tests/base/disputegame_v2/init_test.go @@ -7,5 +7,7 @@ import ( ) func TestMain(m *testing.M) { - presets.DoMain(m, presets.WithMinimal(), presets.WithDisputeGameV2()) + // TODO(#17810): Use the new v2 dispute game flag via presets.WithDisputeGameV2() + //presets.DoMain(m, presets.WithMinimal(), presets.WithDisputeGameV2()) + presets.DoMain(m, presets.WithMinimal()) } diff --git a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol index aaffb5972a3e9..118829029ea63 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol @@ -216,6 +216,8 @@ interface IOPContractsManager { address anchorStateRegistryImpl; address delayedWETHImpl; address mipsImpl; + address faultDisputeGameV2Impl; + address permissionedDisputeGameV2Impl; } /// @notice The input required to identify a chain for upgrading. @@ -304,6 +306,8 @@ interface IOPContractsManager { error PrestateRequired(); + error InvalidDevFeatureAccess(bytes32 devFeature); + // -------- Methods -------- function __constructor__( diff --git a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol index fef559ef973c1..2503d23602e97 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol @@ -157,7 +157,9 @@ contract DeployImplementations is Script { disputeGameFactoryImpl: address(_output.disputeGameFactoryImpl), anchorStateRegistryImpl: address(_output.anchorStateRegistryImpl), delayedWETHImpl: address(_output.delayedWETHImpl), - mipsImpl: address(_output.mipsSingleton) + mipsImpl: address(_output.mipsSingleton), + faultDisputeGameV2Impl: address(_output.faultDisputeGameV2Impl), + permissionedDisputeGameV2Impl: address(_output.permissionedDisputeGameV2Impl) }); deployOPCMBPImplsContainer(_input, _output, _blueprints, implementations); diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 77a3d3d4351f8..7bf5c07456d97 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.15; import { Script } from "forge-std/Script.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { Solarray } from "scripts/libraries/Solarray.sol"; import { ChainAssertions } from "scripts/deploy/ChainAssertions.sol"; @@ -24,6 +25,7 @@ import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; +import { IOPContractsManager } from "../../interfaces/L1/IOPContractsManager.sol"; contract DeployOPChain is Script { struct Output { @@ -120,6 +122,13 @@ contract DeployOPChain is Script { checkOutput(_input, output_); } + // -------- Features -------- + + function isDevFeatureV2DisputeGamesEnabled(address _opcmAddr) internal view returns (bool) { + IOPContractsManager opcm = IOPContractsManager(_opcmAddr); + return DevFeatures.isDevFeatureEnabled(opcm.devFeatureBitmap(), DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + } + // -------- Validations -------- function checkInput(Types.DeployOPChainInput memory _i) public view { @@ -162,13 +171,19 @@ contract DeployOPChain is Script { address(_o.optimismPortalProxy), address(_o.disputeGameFactoryProxy), address(_o.anchorStateRegistryProxy), - address(_o.permissionedDisputeGame), address(_o.delayedWETHPermissionedGameProxy), address(_o.ethLockboxProxy) ); - // TODO: Eventually switch from Permissioned to Permissionless. Add this address back in. - // address(_o.delayedWETHPermissionlessGameProxy) - // address(_o.faultDisputeGame()), + + if (!isDevFeatureV2DisputeGamesEnabled(_i.opcm)) { + // Only check dispute game contracts if v2 dispute games are not enabled. + // When v2 contracts are enabled, we no longer deploy dispute games per chain + addrs2 = Solarray.extend(addrs2, Solarray.addresses(address(_o.permissionedDisputeGame))); + + // TODO: Eventually switch from Permissioned to Permissionless. Add these addresses back in. + // address(_o.delayedWETHPermissionlessGameProxy) + // address(_o.faultDisputeGame()), + } DeployUtils.assertValidContractAddresses(Solarray.extend(addrs1, addrs2)); _assertValidDeploy(_i, _o); @@ -192,10 +207,17 @@ contract DeployOPChain is Script { SuperchainConfig: address(0) }); - ChainAssertions.checkAnchorStateRegistryProxy(_o.anchorStateRegistryProxy, true); + // Check dispute games + address expectedPDGImpl = address(_o.permissionedDisputeGame); + if (isDevFeatureV2DisputeGamesEnabled(_i.opcm)) { + // With v2 game contracts enabled, we use the predeployed pdg implementation + expectedPDGImpl = IOPContractsManager(_i.opcm).implementations().permissionedDisputeGameV2Impl; + } ChainAssertions.checkDisputeGameFactory( - _o.disputeGameFactoryProxy, _i.opChainProxyAdminOwner, address(_o.permissionedDisputeGame), true + _o.disputeGameFactoryProxy, _i.opChainProxyAdminOwner, expectedPDGImpl, true ); + + ChainAssertions.checkAnchorStateRegistryProxy(_o.anchorStateRegistryProxy, true); ChainAssertions.checkL1CrossDomainMessenger(_o.l1CrossDomainMessengerProxy, vm, true); ChainAssertions.checkOptimismPortal2({ _contracts: proxies, diff --git a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol index f67fd6448495a..b8dc9a685b42f 100644 --- a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol @@ -12,6 +12,7 @@ import { LibString } from "@solady/utils/LibString.sol"; import { Process } from "scripts/libraries/Process.sol"; import { Config } from "scripts/libraries/Config.sol"; import { Bytes } from "src/libraries/Bytes.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; @@ -113,6 +114,8 @@ contract VerifyOPCM is Script { fieldNameOverrides["optimismPortalInteropImpl"] = "OptimismPortalInterop"; fieldNameOverrides["mipsImpl"] = "MIPS64"; fieldNameOverrides["ethLockboxImpl"] = "ETHLockbox"; + fieldNameOverrides["faultDisputeGameV2Impl"] = "FaultDisputeGameV2"; + fieldNameOverrides["permissionedDisputeGameV2Impl"] = "PermissionedDisputeGameV2"; fieldNameOverrides["permissionlessDisputeGame1"] = "FaultDisputeGame"; fieldNameOverrides["permissionlessDisputeGame2"] = "FaultDisputeGame"; fieldNameOverrides["permissionedDisputeGame1"] = "PermissionedDisputeGame"; @@ -177,8 +180,14 @@ contract VerifyOPCM is Script { /// @param _addr Address of the contract to verify. /// @param _skipConstructorVerification Whether to skip constructor verification. function runSingle(string memory _name, address _addr, bool _skipConstructorVerification) public { + // This function is used as part of the release checklist to verify new contracts. + // Rather than requiring an opcm input parameter, just pass in an empty reference + // as we really only need this for features that are in development. + IOPContractsManager emptyOpcm = IOPContractsManager(address(0)); _verifyOpcmContractRef( - OpcmContractRef({ field: _name, name: _name, addr: _addr, blueprint: false }), _skipConstructorVerification + emptyOpcm, + OpcmContractRef({ field: _name, name: _name, addr: _addr, blueprint: false }), + _skipConstructorVerification ); } @@ -213,7 +222,7 @@ contract VerifyOPCM is Script { // Verify each reference. bool success = true; for (uint256 i = 0; i < refs.length; i++) { - success = _verifyOpcmContractRef(refs[i], _skipConstructorVerification) && success; + success = _verifyOpcmContractRef(opcm, refs[i], _skipConstructorVerification) && success; } // Final Result @@ -381,16 +390,20 @@ contract VerifyOPCM is Script { } /// @notice Verifies a single OPCM contract reference (implementation or bytecode). + /// @param _opcm The OPCM contract that contains the target contract reference. /// @param _target The target contract reference to verify. /// @param _skipConstructorVerification Whether to skip constructor verification. /// @return True if the contract reference is verified, false otherwise. function _verifyOpcmContractRef( + IOPContractsManager _opcm, OpcmContractRef memory _target, bool _skipConstructorVerification ) internal returns (bool) { + bool success = true; + console.log(); console.log(string.concat("Checking Contract: ", _target.field)); console.log(string.concat(" Type: ", _target.blueprint ? "Blueprint" : "Implementation")); @@ -401,6 +414,20 @@ contract VerifyOPCM is Script { string memory artifactPath = _buildArtifactPath(_target.name); console.log(string.concat(" Expected Runtime Artifact: ", artifactPath)); + // Check if this is a V2 dispute game that should be skipped + if (_isV2DisputeGameImplementation(_target.name)) { + if (!_isV2DisputeGamesEnabled(_opcm)) { + if (_target.addr == address(0)) { + console.log("[SKIP] V2 dispute game not deployed (feature disabled)"); + return true; // Consider this "verified" when feature is off + } else { + console.log("[FAIL] ERROR: V2 dispute game deployed but feature disabled"); + success = false; + } + } + // If feature is enabled, continue with normal verification + } + // Load artifact information (bytecode, immutable refs) for detailed comparison ArtifactInfo memory artifact = _loadArtifactInfo(artifactPath); @@ -442,7 +469,7 @@ contract VerifyOPCM is Script { } // Perform detailed bytecode comparison. - bool success = _compareBytecode(actualCode, expectedCode, _target.name, artifact, !_target.blueprint); + success = _compareBytecode(actualCode, expectedCode, _target.name, artifact, !_target.blueprint) && success; // If requested and this is not a blueprint, we also need to check the creation code. if (!_target.blueprint && !_skipConstructorVerification) { @@ -497,6 +524,22 @@ contract VerifyOPCM is Script { return success; } + /// @notice Checks if V2 dispute games feature is enabled in the dev feature bitmap. + /// @param _opcm The OPContractsManager to check. + /// @return True if V2 dispute games are enabled. + function _isV2DisputeGamesEnabled(IOPContractsManager _opcm) internal view returns (bool) { + bytes32 bitmap = _opcm.devFeatureBitmap(); + return DevFeatures.isDevFeatureEnabled(bitmap, DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + } + + /// @notice Checks if a contract is a V2 dispute game implementation. + /// @param _contractName The name to check. + /// @return True if this is a V2 dispute game. + function _isV2DisputeGameImplementation(string memory _contractName) internal pure returns (bool) { + return LibString.eq(_contractName, "FaultDisputeGameV2") + || LibString.eq(_contractName, "PermissionedDisputeGameV2"); + } + /// @notice Verifies that the immutable variables in the OPCM contract match expected values. /// @param _opcm The OPCM contract to verify immutable variables for. /// @return True if all immutable variables are verified, false otherwise. diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json index b9e54537a4d33..5372533426d4e 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json @@ -525,6 +525,16 @@ "internalType": "address", "name": "mipsImpl", "type": "address" + }, + { + "internalType": "address", + "name": "faultDisputeGameV2Impl", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGameV2Impl", + "type": "address" } ], "internalType": "struct OPContractsManager.Implementations", @@ -991,6 +1001,17 @@ "name": "InvalidChainId", "type": "error" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "devFeature", + "type": "bytes32" + } + ], + "name": "InvalidDevFeatureAccess", + "type": "error" + }, { "inputs": [], "name": "InvalidGameConfigs", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContractsContainer.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContractsContainer.json index c49484fc7224c..6b4c6228c2736 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContractsContainer.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContractsContainer.json @@ -144,6 +144,16 @@ "internalType": "address", "name": "mipsImpl", "type": "address" + }, + { + "internalType": "address", + "name": "faultDisputeGameV2Impl", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGameV2Impl", + "type": "address" } ], "internalType": "struct OPContractsManager.Implementations", @@ -340,6 +350,16 @@ "internalType": "address", "name": "mipsImpl", "type": "address" + }, + { + "internalType": "address", + "name": "faultDisputeGameV2Impl", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGameV2Impl", + "type": "address" } ], "internalType": "struct OPContractsManager.Implementations", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json index 7cd7a44502c06..08b6612c99af7 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json @@ -428,6 +428,16 @@ "internalType": "address", "name": "mipsImpl", "type": "address" + }, + { + "internalType": "address", + "name": "faultDisputeGameV2Impl", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGameV2Impl", + "type": "address" } ], "internalType": "struct OPContractsManager.Implementations", @@ -518,6 +528,17 @@ "name": "InvalidChainId", "type": "error" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "devFeature", + "type": "bytes32" + } + ], + "name": "InvalidDevFeatureAccess", + "type": "error" + }, { "inputs": [ { diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json index 6bf4393418702..5a513822ef9ab 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json @@ -321,6 +321,16 @@ "internalType": "address", "name": "mipsImpl", "type": "address" + }, + { + "internalType": "address", + "name": "faultDisputeGameV2Impl", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGameV2Impl", + "type": "address" } ], "internalType": "struct OPContractsManager.Implementations", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInteropMigrator.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInteropMigrator.json index b06cd541bb38d..e8d1dec02c225 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInteropMigrator.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInteropMigrator.json @@ -223,6 +223,16 @@ "internalType": "address", "name": "mipsImpl", "type": "address" + }, + { + "internalType": "address", + "name": "faultDisputeGameV2Impl", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGameV2Impl", + "type": "address" } ], "internalType": "struct OPContractsManager.Implementations", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json index 512a83ae75cff..a092f31799459 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json @@ -223,6 +223,16 @@ "internalType": "address", "name": "mipsImpl", "type": "address" + }, + { + "internalType": "address", + "name": "faultDisputeGameV2Impl", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGameV2Impl", + "type": "address" } ], "internalType": "struct OPContractsManager.Implementations", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index c0eb064250f48..40f3534999ba2 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -20,8 +20,8 @@ "sourceCodeHash": "0xfca613b5d055ffc4c3cbccb0773ddb9030abedc1aa6508c9e2e7727cc0cd617b" }, "src/L1/OPContractsManager.sol:OPContractsManager": { - "initCodeHash": "0x9f9a3738b05cae6597ea9a5c5747f7dbd3a5328b05a319955054fbd8b1aaa791", - "sourceCodeHash": "0x154c764083f353e2a56337c0dd5cbcd6f2e12c21966cd0580c7a0f96c4e147dd" + "initCodeHash": "0x42721744f90fa46ee680fecc69da2e5caf7fdd8093c2a7f3b33958e574a15579", + "sourceCodeHash": "0x3eab23f3f034eec77afb620a122e51fded9214b5ed6a4c5663e0174714ae0f5e" }, "src/L1/OPContractsManagerStandardValidator.sol:OPContractsManagerStandardValidator": { "initCodeHash": "0x57d6a6729d887ead009d518e8f17fa0d26bfc97b8efe1494ab4ef8dbb000d109", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContractsContainer.json b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContractsContainer.json index d87deb94bc76b..2193053869827 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContractsContainer.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContractsContainer.json @@ -7,7 +7,7 @@ "type": "struct OPContractsManager.Blueprints" }, { - "bytes": "448", + "bytes": "512", "label": "implementation", "offset": 0, "slot": "13", diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 142fa67ea358f..f72daa376a22c 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -306,6 +306,35 @@ abstract contract OPContractsManagerBase { return _disputeGame.challenger(); } + /// @notice Helper function to register permissioned game V2 implementation + /// @dev Extracted to avoid stack too deep error + /// @param _input The deployment input data containing all necessary parameters + /// @param _implementation The implementation addresses struct + /// @param _output The deployment output containing proxy addresses + function _registerPermissionedGameV2( + OPContractsManager.DeployInput calldata _input, + OPContractsManager.Implementations memory _implementation, + OPContractsManager.DeployOutput memory _output + ) + internal + { + bytes memory gameArgs = abi.encodePacked( + _input.disputeAbsolutePrestate, // 32 bytes + _implementation.mipsImpl, // 20 bytes + address(_output.anchorStateRegistryProxy), // 20 bytes + address(_output.delayedWETHPermissionedGameProxy), // 20 bytes + _input.l2ChainId, // 32 bytes + _input.roles.proposer, // 20 bytes + _input.roles.challenger // 20 bytes + ); + setDGFImplementation( + _output.disputeGameFactoryProxy, + GameTypes.PERMISSIONED_CANNON, + IDisputeGame(_implementation.permissionedDisputeGameV2Impl), + gameArgs + ); + } + /// @notice Retrieves the DisputeGameFactory address for a given SystemConfig function getDisputeGameFactory(ISystemConfig _systemConfig) internal view returns (IDisputeGameFactory) { return IDisputeGameFactory(_systemConfig.disputeGameFactory()); @@ -360,9 +389,31 @@ abstract contract OPContractsManagerBase { } /// @notice Sets a game implementation on the dispute game factory + /// @param _dgf The dispute game factory + /// @param _gameType The game type + /// @param _newGame The new game implementation function setDGFImplementation(IDisputeGameFactory _dgf, GameType _gameType, IDisputeGame _newGame) internal { _dgf.setImplementation(_gameType, _newGame); } + + /// @notice Sets a game implementation on the dispute game factory + /// @param _dgf The dispute game factory + /// @param _gameType The game type + /// @param _newGame The new game implementation + /// @param _gameArgs Game arguments for this game type + function setDGFImplementation( + IDisputeGameFactory _dgf, + GameType _gameType, + IDisputeGame _newGame, + bytes memory _gameArgs + ) + internal + { + if (!isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { + revert OPContractsManager.InvalidDevFeatureAccess(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + } + _dgf.setImplementation(_gameType, _newGame, _gameArgs); + } } contract OPContractsManagerGameTypeAdder is OPContractsManagerBase { @@ -1064,30 +1115,31 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { ) ); - // While not a proxy, we deploy the PermissionedDisputeGame here as well because it's bespoke per chain. - output.permissionedDisputeGame = IPermissionedDisputeGame( - Blueprint.deployFrom( - blueprint.permissionedDisputeGame1, - blueprint.permissionedDisputeGame2, - computeSalt(_input.l2ChainId, _input.saltMixer, "PermissionedDisputeGame"), - encodePermissionedFDGConstructor( - IFaultDisputeGame.GameConstructorParams({ - gameType: GameTypes.PERMISSIONED_CANNON, - absolutePrestate: _input.disputeAbsolutePrestate, - maxGameDepth: _input.disputeMaxGameDepth, - splitDepth: _input.disputeSplitDepth, - clockExtension: _input.disputeClockExtension, - maxClockDuration: _input.disputeMaxClockDuration, - vm: IBigStepper(implementation.mipsImpl), - weth: IDelayedWETH(payable(address(output.delayedWETHPermissionedGameProxy))), - anchorStateRegistry: IAnchorStateRegistry(address(output.anchorStateRegistryProxy)), - l2ChainId: _input.l2ChainId - }), - _input.roles.proposer, - _input.roles.challenger + if (!isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { + output.permissionedDisputeGame = IPermissionedDisputeGame( + Blueprint.deployFrom( + blueprint.permissionedDisputeGame1, + blueprint.permissionedDisputeGame2, + computeSalt(_input.l2ChainId, _input.saltMixer, "PermissionedDisputeGame"), + encodePermissionedFDGConstructor( + IFaultDisputeGame.GameConstructorParams({ + gameType: GameTypes.PERMISSIONED_CANNON, + absolutePrestate: _input.disputeAbsolutePrestate, + maxGameDepth: _input.disputeMaxGameDepth, + splitDepth: _input.disputeSplitDepth, + clockExtension: _input.disputeClockExtension, + maxClockDuration: _input.disputeMaxClockDuration, + vm: IBigStepper(implementation.mipsImpl), + weth: IDelayedWETH(payable(address(output.delayedWETHPermissionedGameProxy))), + anchorStateRegistry: IAnchorStateRegistry(address(output.anchorStateRegistryProxy)), + l2ChainId: _input.l2ChainId + }), + _input.roles.proposer, + _input.roles.challenger + ) ) - ) - ); + ); + } // -------- Set and Initialize Proxy Implementations -------- bytes memory data; @@ -1173,11 +1225,18 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { implementation.disputeGameFactoryImpl, data ); - setDGFImplementation( - output.disputeGameFactoryProxy, - GameTypes.PERMISSIONED_CANNON, - IDisputeGame(address(output.permissionedDisputeGame)) - ); + // Register the appropriate dispute game implementation based on the feature flag + if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { + // Extracted to helper function to avoid stack too deep error + _registerPermissionedGameV2(_input, implementation, output); + } else { + // Register v1 implementation for PERMISSIONED_CANNON game type + setDGFImplementation( + output.disputeGameFactoryProxy, + GameTypes.PERMISSIONED_CANNON, + IDisputeGame(address(output.permissionedDisputeGame)) + ); + } transferOwnership(address(output.disputeGameFactoryProxy), address(_input.roles.opChainProxyAdminOwner)); @@ -1798,6 +1857,8 @@ contract OPContractsManager is ISemver { address anchorStateRegistryImpl; address delayedWETHImpl; address mipsImpl; + address faultDisputeGameV2Impl; + address permissionedDisputeGameV2Impl; } /// @notice The input required to identify a chain for upgrading, along with new prestate hashes @@ -1837,9 +1898,9 @@ contract OPContractsManager is ISemver { // -------- Constants and Variables -------- - /// @custom:semver 4.0.0 + /// @custom:semver 4.1.0 function version() public pure virtual returns (string memory) { - return "4.0.0"; + return "4.1.0"; } OPContractsManagerGameTypeAdder public immutable opcmGameTypeAdder; @@ -1906,6 +1967,9 @@ contract OPContractsManager is ISemver { /// @notice Thrown when the prestate of a permissioned disputed game is 0. error PrestateRequired(); + /// @notice Thrown if logic gated by a dev feature flag is incorrectly accessed. + error InvalidDevFeatureAccess(bytes32 devFeature); + // -------- Methods -------- constructor( diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index e2ffb7ed48f03..babd8742b5a3e 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -5,6 +5,7 @@ pragma solidity 0.8.15; import { Test, stdStorage, StdStorage } from "forge-std/Test.sol"; import { VmSafe } from "forge-std/Vm.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; +import { FeatureFlags } from "test/setup/FeatureFlags.sol"; import { DeployOPChain_TestBase } from "test/opcm/DeployOPChain.t.sol"; import { DelegateCaller } from "test/mocks/Callers.sol"; @@ -417,7 +418,7 @@ contract OPContractsManager_TestInit is CommonTest { /// @title OPContractsManager_ChainIdToBatchInboxAddress_Test /// @notice Tests the `chainIdToBatchInboxAddress` function of the `OPContractsManager` contract. /// @dev These tests use the harness which exposes internal functions for testing. -contract OPContractsManager_ChainIdToBatchInboxAddress_Test is Test { +contract OPContractsManager_ChainIdToBatchInboxAddress_Test is Test, FeatureFlags { OPContractsManager_Harness opcmHarness; address challenger = makeAddr("challenger"); @@ -430,8 +431,9 @@ contract OPContractsManager_ChainIdToBatchInboxAddress_Test is Test { vm.etch(address(superchainConfigProxy), hex"01"); vm.etch(address(protocolVersionsProxy), hex"01"); + resolveFeaturesFromEnv(); OPContractsManagerContractsContainer container = - new OPContractsManagerContractsContainer(emptyBlueprints, emptyImpls, bytes32(0)); + new OPContractsManagerContractsContainer(emptyBlueprints, emptyImpls, devFeatureBitmap); OPContractsManager.Implementations memory __opcmImplementations = container.implementations(); OPContractsManagerStandardValidator.Implementations memory opcmImplementations; @@ -472,6 +474,14 @@ contract OPContractsManager_ChainIdToBatchInboxAddress_Test is Test { /// @title OPContractsManager_AddGameType_Test /// @notice Tests the `addGameType` function of the `OPContractsManager` contract. contract OPContractsManager_AddGameType_Test is OPContractsManager_TestInit { + function setUp() public virtual override { + super.setUp(); + + // Skip AddGameType tests when V2 dispute games are enabled + // TODO(#17260): Remove skip when V2 dispute game support for addGameType implemented + skipIfDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + } + /// @notice Tests that we can add a PermissionedDisputeGame implementation with addGameType. function test_addGameType_permissioned_succeeds() public { // Create the input for the Permissioned game type. @@ -772,6 +782,12 @@ contract OPContractsManager_UpdatePrestate_Test is OPContractsManager_TestInit { function setUp() public virtual override { super.setUp(); + + // Skip UpdatePrestate tests when V2 dispute games enabled + // UpdatePrestate feature not yet implemented for V2 + // TODO(#17261): Remove skip when V2 dispute game support for updatePrestate implemented + skipIfDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + prestateUpdater = opcm; } @@ -1893,6 +1909,35 @@ contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase { }); } + /// @notice Helper function to create a permissioned game through the factory + function _createPermissionedGame( + IDisputeGameFactory factory, + address proposer, + Claim claim, + uint256 l2BlockNumber + ) + internal + returns (IPermissionedDisputeGame) + { + // Check if there's an init bond required for the game type + uint256 initBond = factory.initBonds(GameTypes.PERMISSIONED_CANNON); + + // Fund the proposer if needed + if (initBond > 0) { + vm.deal(proposer, initBond); + } + + // We use vm.startPrank to set both msg.sender and tx.origin to the proposer + vm.startPrank(proposer, proposer); + + IDisputeGame gameProxy = + factory.create{ value: initBond }(GameTypes.PERMISSIONED_CANNON, claim, abi.encode(bytes32(l2BlockNumber))); + + vm.stopPrank(); + + return IPermissionedDisputeGame(address(gameProxy)); + } + function test_deploy_l2ChainIdEqualsZero_reverts() public { IOPContractsManager.DeployInput memory input = toOPCMDeployInput(deployOPChainInput); input.l2ChainId = 0; @@ -1914,20 +1959,72 @@ contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase { emit Deployed(deployOPChainInput.l2ChainId, address(this), bytes("")); opcm.deploy(toOPCMDeployInput(deployOPChainInput)); } + + /// @notice Test that deploy sets the permissioned dispute game implementation + function test_deployPermissioned_succeeds() public { + bool isV2 = isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + + // Sanity-check setup is consistent with devFeatures flag + IOPContractsManager.Implementations memory impls = opcm.implementations(); + address pdgImpl = address(impls.permissionedDisputeGameV2Impl); + address fdgImpl = address(impls.faultDisputeGameV2Impl); + if (isV2) { + assertFalse(pdgImpl == address(0), "PDG implementation address should be non-zero"); + assertFalse(fdgImpl == address(0), "FDG implementation address should be non-zero"); + } else { + assertTrue(pdgImpl == address(0), "PDG implementation address should be zero"); + assertTrue(fdgImpl == address(0), "FDG implementation address should be zero"); + } + + // Run OPCM.deploy + IOPContractsManager.DeployInput memory opcmInput = toOPCMDeployInput(deployOPChainInput); + IOPContractsManager.DeployOutput memory opcmOutput = opcm.deploy(opcmInput); + + // Verify that the DisputeGameFactory has registered an implementation for the PERMISSIONED_CANNON game type + address expectedPDGAddress = isV2 ? pdgImpl : address(opcmOutput.permissionedDisputeGame); + address actualPDGAddress = address(opcmOutput.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)); + assertNotEq(actualPDGAddress, address(0), "DisputeGameFactory should have a registered PERMISSIONED_CANNON"); + assertEq(actualPDGAddress, address(expectedPDGAddress), "PDG address should match"); + + // Create a game proxy to test immutable fields + Claim claim = Claim.wrap(bytes32(uint256(9876))); + uint256 l2BlockNumber = uint256(123); + IPermissionedDisputeGame pdg = + _createPermissionedGame(opcmOutput.disputeGameFactoryProxy, opcmInput.roles.proposer, claim, l2BlockNumber); + + // Verify immutable fields on the game proxy + // Constructor args + assertEq(pdg.gameType().raw(), GameTypes.PERMISSIONED_CANNON.raw(), "Game type should match"); + assertEq(pdg.clockExtension().raw(), opcmInput.disputeClockExtension.raw(), "Clock extension should match"); + assertEq( + pdg.maxClockDuration().raw(), opcmInput.disputeMaxClockDuration.raw(), "Max clock duration should match" + ); + assertEq(pdg.splitDepth(), opcmInput.disputeSplitDepth, "Split depth should match"); + assertEq(pdg.maxGameDepth(), opcmInput.disputeMaxGameDepth, "Max game depth should match"); + // Clone-with-immutable-args + assertEq(pdg.gameCreator(), opcmInput.roles.proposer, "Game creator should match"); + assertEq(pdg.rootClaim().raw(), claim.raw(), "Claim should match"); + assertEq(pdg.l1Head().raw(), blockhash(block.number - 1), "L1 head should match"); + assertEq(pdg.l2BlockNumber(), l2BlockNumber, "L2 Block number should match"); + assertEq( + pdg.absolutePrestate().raw(), + opcmInput.disputeAbsolutePrestate.raw(), + "Absolute prestate should match input" + ); + assertEq(address(pdg.vm()), address(impls.mipsImpl), "VM should match MIPS implementation"); + assertEq(address(pdg.anchorStateRegistry()), address(opcmOutput.anchorStateRegistryProxy), "ASR should match"); + assertEq(address(pdg.weth()), address(opcmOutput.delayedWETHPermissionedGameProxy), "WETH should match"); + assertEq(pdg.l2ChainId(), opcmInput.l2ChainId, "L2 chain ID should match"); + // For permissioned game, check proposer and challenger + assertEq(pdg.proposer(), opcmInput.roles.proposer, "Proposer should match"); + assertEq(pdg.challenger(), opcmInput.roles.challenger, "Challenger should match"); + } } /// @title OPContractsManager_Version_Test /// @notice Tests the `version` function of the `OPContractsManager` contract. contract OPContractsManager_Version_Test is OPContractsManager_TestInit { - IOPContractsManager internal prestateUpdater; - OPContractsManager.AddGameInput[] internal gameInput; - - function setUp() public override { - super.setUp(); - prestateUpdater = opcm; - } - function test_semver_works() public view { - assertNotEq(abi.encode(prestateUpdater.version()), abi.encode(0)); + assertNotEq(abi.encode(opcm.version()), abi.encode(0)); } } diff --git a/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol index 8bbbd9e8c2cd7..c516d46843fb7 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol @@ -10,6 +10,7 @@ import { GameTypes, Duration, Claim } from "src/dispute/lib/Types.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { ForgeArtifacts } from "scripts/libraries/ForgeArtifacts.sol"; import { Features } from "src/libraries/Features.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; @@ -102,6 +103,10 @@ contract OPContractsManagerStandardValidator_TestInit is CommonTest { function setUp() public virtual override { super.setUp(); + // Skip V1 StandardValidator tests when V2 dispute games are enabled + // TODO(#17267): Remove skip when V2 dispute game support added to the StandardValidator is implemented + skipIfDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + // Grab the deploy input for later use. deployInput = deploy.getDeployInput(); diff --git a/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol b/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol index 58b4e6e624a22..403f5b1428895 100644 --- a/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol +++ b/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol @@ -11,6 +11,7 @@ import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; // Libraries import "src/dispute/lib/Types.sol"; import "src/dispute/lib/Errors.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; @@ -82,7 +83,8 @@ contract DisputeGameFactory_TestInit is CommonTest { function _getGameConstructorParams( Claim _absolutePrestate, AlphabetVM _vm, - GameType _gameType + GameType _gameType, + uint256 _l2ChainId ) internal view @@ -98,7 +100,7 @@ contract DisputeGameFactory_TestInit is CommonTest { vm: _vm, weth: delayedWeth, anchorStateRegistry: anchorStateRegistry, - l2ChainId: 0 + l2ChainId: _l2ChainId }); } @@ -125,7 +127,7 @@ contract DisputeGameFactory_TestInit is CommonTest { view returns (ISuperFaultDisputeGame.GameConstructorParams memory params_) { - bytes memory args = abi.encode(_getGameConstructorParams(_absolutePrestate, _vm, _gameType)); + bytes memory args = abi.encode(_getGameConstructorParams(_absolutePrestate, _vm, _gameType, 0)); params_ = abi.decode(args, (ISuperFaultDisputeGame.GameConstructorParams)); } @@ -200,13 +202,26 @@ contract DisputeGameFactory_TestInit is CommonTest { function setupFaultDisputeGame(Claim _absolutePrestate) internal returns (address gameImpl_, AlphabetVM vm_, IPreimageOracle preimageOracle_) + { + if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { + return setupFaultDisputeGameV2(_absolutePrestate); + } else { + return setupFaultDisputeGameV1(_absolutePrestate); + } + } + + /// @notice Sets up a fault game implementation + function setupFaultDisputeGameV1(Claim _absolutePrestate) + internal + returns (address gameImpl_, AlphabetVM vm_, IPreimageOracle preimageOracle_) { (vm_, preimageOracle_) = _createVM(_absolutePrestate); gameImpl_ = DeployUtils.create1({ _name: "FaultDisputeGame", _args: DeployUtils.encodeConstructor( abi.encodeCall( - IFaultDisputeGame.__constructor__, (_getGameConstructorParams(_absolutePrestate, vm_, GameTypes.CANNON)) + IFaultDisputeGame.__constructor__, + (_getGameConstructorParams(_absolutePrestate, vm_, GameTypes.CANNON, l2ChainId)) ) ) }); @@ -258,6 +273,21 @@ contract DisputeGameFactory_TestInit is CommonTest { ) internal returns (address gameImpl_, AlphabetVM vm_, IPreimageOracle preimageOracle_) + { + if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { + return setupPermissionedDisputeGameV2(_absolutePrestate, _proposer, _challenger); + } else { + return setupPermissionedDisputeGameV1(_absolutePrestate, _proposer, _challenger); + } + } + + function setupPermissionedDisputeGameV1( + Claim _absolutePrestate, + address _proposer, + address _challenger + ) + internal + returns (address gameImpl_, AlphabetVM vm_, IPreimageOracle preimageOracle_) { (vm_, preimageOracle_) = _createVM(_absolutePrestate); gameImpl_ = DeployUtils.create1({ @@ -266,7 +296,7 @@ contract DisputeGameFactory_TestInit is CommonTest { abi.encodeCall( IPermissionedDisputeGame.__constructor__, ( - _getGameConstructorParams(_absolutePrestate, vm_, GameTypes.PERMISSIONED_CANNON), + _getGameConstructorParams(_absolutePrestate, vm_, GameTypes.PERMISSIONED_CANNON, l2ChainId), _proposer, _challenger ) @@ -576,7 +606,6 @@ contract DisputeGameFactory_SetImplementation_Test is DisputeGameFactory_TestIni AlphabetVM vm_; IPreimageOracle preimageOracle_; (vm_, preimageOracle_) = _createVM(absolutePrestate); - uint256 l2ChainId = 111; bytes memory args = abi.encodePacked( absolutePrestate, // 32 bytes diff --git a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol index 686b4489f5dad..a8a4b99e479d4 100644 --- a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol @@ -21,12 +21,14 @@ import { LibClock } from "src/dispute/lib/LibUDT.sol"; import { LibPosition } from "src/dispute/lib/LibPosition.sol"; import "src/dispute/lib/Types.sol"; import "src/dispute/lib/Errors.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IPreimageOracle } from "interfaces/dispute/IBigStepper.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; +import { IFaultDisputeGameV2 } from "interfaces/dispute/v2/IFaultDisputeGameV2.sol"; import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; contract ClaimCreditReenter { @@ -119,6 +121,8 @@ contract BaseFaultDisputeGame_TestInit is DisputeGameFactory_TestInit { assertEq(address(gameProxy.weth()), address(delayedWeth)); assertEq(address(gameProxy.anchorStateRegistry()), address(anchorStateRegistry)); assertEq(address(gameProxy.vm()), address(_vm)); + assertEq(address(gameProxy.gameCreator()), address(this)); + assertEq(gameProxy.l2ChainId(), l2ChainId); // Label the proxy vm.label(address(gameProxy), "FaultDisputeGame_Clone"); @@ -127,6 +131,14 @@ contract BaseFaultDisputeGame_TestInit is DisputeGameFactory_TestInit { fallback() external payable { } receive() external payable { } + + function copyBytes(bytes memory src, bytes memory dest) internal pure returns (bytes memory) { + uint256 byteCount = src.length < dest.length ? src.length : dest.length; + for (uint256 i = 0; i < byteCount; i++) { + dest[i] = src[i]; + } + return dest; + } } /// @title FaultDisputeGame_TestInit @@ -224,6 +236,11 @@ contract FaultDisputeGame_Version_Test is FaultDisputeGame_TestInit { /// @title FaultDisputeGame_Constructor_Test /// @notice Tests the constructor of the `FaultDisputeGame` contract. contract FaultDisputeGame_Constructor_Test is FaultDisputeGame_TestInit { + function setUp() public virtual override { + super.setUp(); + skipIfDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + } + /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when the /// `MAX_GAME_DEPTH` parameter is greater than `LibPosition.MAX_POSITION_BITLEN - 1`. function testFuzz_constructor_maxDepthTooLarge_reverts(uint256 _maxGameDepth) public { @@ -472,6 +489,145 @@ contract FaultDisputeGame_Constructor_Test is FaultDisputeGame_TestInit { } } +/// @title FaultDisputeGame_Constructor_Test +/// @notice Tests the constructor of the `FaultDisputeGame` contract. +contract FaultDisputeGameV2_Constructor_Test is FaultDisputeGame_TestInit { + function setUp() public virtual override { + super.setUp(); + skipIfDevFeatureDisabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + } + + /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when the + /// `MAX_GAME_DEPTH` parameter is greater than `LibPosition.MAX_POSITION_BITLEN - 1`. + function testFuzz_constructor_maxDepthTooLarge_reverts(uint256 _maxGameDepth) public { + _maxGameDepth = bound(_maxGameDepth, LibPosition.MAX_POSITION_BITLEN, type(uint256).max - 1); + vm.expectRevert(MaxDepthTooLarge.selector); + DeployUtils.create1({ + _name: "FaultDisputeGameV2", + _args: DeployUtils.encodeConstructor( + abi.encodeCall( + IFaultDisputeGameV2.__constructor__, + ( + IFaultDisputeGameV2.GameConstructorParams({ + gameType: GAME_TYPE, + maxGameDepth: _maxGameDepth, + splitDepth: _maxGameDepth + 1, + clockExtension: Duration.wrap(3 hours), + maxClockDuration: Duration.wrap(3.5 days) + }) + ) + ) + ) + }); + } + + /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when the `_splitDepth` + /// parameter is greater than or equal to the `MAX_GAME_DEPTH` + function testFuzz_constructor_invalidSplitDepth_reverts(uint256 _splitDepth) public { + uint256 maxGameDepth = 2 ** 3; + _splitDepth = bound(_splitDepth, maxGameDepth - 1, type(uint256).max); + vm.expectRevert(InvalidSplitDepth.selector); + DeployUtils.create1({ + _name: "FaultDisputeGameV2", + _args: DeployUtils.encodeConstructor( + abi.encodeCall( + IFaultDisputeGameV2.__constructor__, + ( + IFaultDisputeGameV2.GameConstructorParams({ + gameType: GAME_TYPE, + maxGameDepth: maxGameDepth, + splitDepth: _splitDepth, + clockExtension: Duration.wrap(3 hours), + maxClockDuration: Duration.wrap(3.5 days) + }) + ) + ) + ) + }); + } + + /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when the `_splitDepth` + /// parameter is less than the minimum split depth (currently 2). + function testFuzz_constructor_lowSplitDepth_reverts(uint256 _splitDepth) public { + uint256 minSplitDepth = 2; + _splitDepth = bound(_splitDepth, 0, minSplitDepth - 1); + vm.expectRevert(InvalidSplitDepth.selector); + DeployUtils.create1({ + _name: "FaultDisputeGameV2", + _args: DeployUtils.encodeConstructor( + abi.encodeCall( + IFaultDisputeGameV2.__constructor__, + ( + IFaultDisputeGameV2.GameConstructorParams({ + gameType: GAME_TYPE, + maxGameDepth: 2 ** 3, + splitDepth: _splitDepth, + clockExtension: Duration.wrap(3 hours), + maxClockDuration: Duration.wrap(3.5 days) + }) + ) + ) + ) + }); + } + + /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when clock + /// extension * 2 is greater than the max clock duration. + function testFuzz_constructor_clockExtensionTooLong_reverts( + uint64 _maxClockDuration, + uint64 _clockExtension + ) + public + { + // Force the clock extension * 2 to be greater than the max clock duration, but keep things + // within bounds of the uint64 type. + _maxClockDuration = uint64(bound(_maxClockDuration, 0, type(uint64).max / 2 - 1)); + _clockExtension = uint64(bound(_clockExtension, _maxClockDuration / 2 + 1, type(uint64).max / 2)); + + vm.expectRevert(InvalidClockExtension.selector); + DeployUtils.create1({ + _name: "FaultDisputeGameV2", + _args: DeployUtils.encodeConstructor( + abi.encodeCall( + IFaultDisputeGameV2.__constructor__, + ( + IFaultDisputeGameV2.GameConstructorParams({ + gameType: GAME_TYPE, + maxGameDepth: 16, + splitDepth: 8, + clockExtension: Duration.wrap(_clockExtension), + maxClockDuration: Duration.wrap(_maxClockDuration) + }) + ) + ) + ) + }); + } + + /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when the `_gameType` + /// parameter is set to the reserved `type(uint32).max` game type. + function test_constructor_reservedGameType_reverts() public { + vm.expectRevert(ReservedGameType.selector); + DeployUtils.create1({ + _name: "FaultDisputeGameV2", + _args: DeployUtils.encodeConstructor( + abi.encodeCall( + IFaultDisputeGameV2.__constructor__, + ( + IFaultDisputeGameV2.GameConstructorParams({ + gameType: GameType.wrap(type(uint32).max), + maxGameDepth: 16, + splitDepth: 8, + clockExtension: Duration.wrap(3 hours), + maxClockDuration: Duration.wrap(3.5 days) + }) + ) + ) + ) + }); + } +} + /// @title FaultDisputeGame_Initialize_Test /// @notice Tests the initialization of the `FaultDisputeGame` contract. contract FaultDisputeGame_Initialize_Test is FaultDisputeGame_TestInit { @@ -507,9 +663,9 @@ contract FaultDisputeGame_Initialize_Test is FaultDisputeGame_TestInit { assertEq(delayedWeth.balanceOf(address(gameProxy)), _value); } - /// @notice Tests that the game cannot be initialized with extra data of the incorrect length - /// (must be 32 bytes) - function testFuzz_initialize_badExtraData_reverts(uint256 _extraDataLen) public { + /// @notice Tests that the game cannot be initialized with incorrect CWIA calldata length + /// caused by extraData of the wrong length + function test_initialize_wrongExtradataLength_reverts(uint256 _extraDataLen) public { // The `DisputeGameFactory` will pack the root claim and the extra data into a single // array, which is enforced to be at least 64 bytes long. // We bound the upper end to 23.5KB to ensure that the minimal proxy never surpasses the @@ -536,6 +692,55 @@ contract FaultDisputeGame_Initialize_Test is FaultDisputeGame_TestInit { ); } + /// @notice Tests that the game cannot be initialized with incorrect CWIA calldata length + /// caused by additional immutable args data + function test_initialize_extraImmutableArgsBytes_reverts(uint256 _extraByteCount) public { + skipIfDevFeatureDisabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + (bytes memory correctArgs,,) = getFaultDisputeGameV2ImmutableArgs(absolutePrestate); + + // We bound the upper end to 23.5KB to ensure that the minimal proxy never surpasses the + // contract size limit in this test, as CWIA proxies store the immutable args in their + // bytecode. + _extraByteCount = bound(_extraByteCount, 1, 23_500); + bytes memory immutableArgs = new bytes(_extraByteCount + correctArgs.length); + // Copy correct args into immutable args + copyBytes(correctArgs, immutableArgs); + + // Set up dispute game implementation with target immutableArgs + setupFaultDisputeGameV2(immutableArgs); + + Claim claim = _dummyClaim(); + vm.expectRevert(IFaultDisputeGame.BadExtraData.selector); + gameProxy = IFaultDisputeGame( + payable( + address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, abi.encode(validL2BlockNumber))) + ) + ); + } + + /// @notice Tests that the game cannot be initialized with incorrect CWIA calldata length + /// caused by missing immutable args data + function test_initialize_missingImmutableArgsBytes_reverts(uint256 _truncatedByteCount) public { + skipIfDevFeatureDisabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + (bytes memory correctArgs,,) = getFaultDisputeGameV2ImmutableArgs(absolutePrestate); + + _truncatedByteCount = (_truncatedByteCount % correctArgs.length) + 1; + bytes memory immutableArgs = new bytes(correctArgs.length - _truncatedByteCount); + // Copy correct args into immutable args + copyBytes(correctArgs, immutableArgs); + + // Set up dispute game implementation with target immutableArgs + setupFaultDisputeGameV2(immutableArgs); + + Claim claim = _dummyClaim(); + vm.expectRevert(IFaultDisputeGame.BadExtraData.selector); + gameProxy = IFaultDisputeGame( + payable( + address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, abi.encode(validL2BlockNumber))) + ) + ); + } + /// @notice Tests that the game is initialized with the correct data. function test_initialize_correctData_succeeds() public view { // Assert that the root claim is initialized correctly. @@ -575,7 +780,9 @@ contract FaultDisputeGame_Initialize_Test is FaultDisputeGame_TestInit { // Creation should fail. vm.expectRevert(AnchorRootNotFound.selector); gameProxy = IFaultDisputeGame( - payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, _dummyClaim(), hex""))) + payable( + address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, _dummyClaim(), new bytes(uint256(32)))) + ) ); } @@ -584,6 +791,35 @@ contract FaultDisputeGame_Initialize_Test is FaultDisputeGame_TestInit { vm.expectRevert(AlreadyInitialized.selector); gameProxy.initialize(); } + + /// @notice Tests that initialization reverts when oracle challenge period is too large. + /// @dev V2 validates oracle challenge period during initialize(), not constructor + function testFuzz_initialize_oracleChallengePeriodTooLarge_reverts(uint256 _challengePeriod) public { + // Bound to values larger than uint64.max + _challengePeriod = bound(_challengePeriod, uint256(type(uint64).max) + 1, type(uint256).max); + + // Get the current AlphabetVM from the setup + (, AlphabetVM vm_,) = setupFaultDisputeGameV2(absolutePrestate); + + // Mock the VM's oracle to return invalid challenge period + vm.mockCall( + address(vm_.oracle()), abi.encodeCall(IPreimageOracle.challengePeriod, ()), abi.encode(_challengePeriod) + ); + + // Expect the initialize call to revert with InvalidChallengePeriod + vm.expectRevert(InvalidChallengePeriod.selector); + + // Create game via factory - initialize() is called automatically and should revert + gameProxy = IFaultDisputeGame( + payable( + address( + disputeGameFactory.create{ value: initBond }( + GAME_TYPE, _dummyClaim(), abi.encode(validL2BlockNumber) + ) + ) + ) + ); + } } /// @title FaultDisputeGame_Step_Test @@ -666,7 +902,7 @@ contract FaultDisputeGame_Step_Test is FaultDisputeGame_TestInit { gameProxy.attack{ value: _getRequiredBond(4) }(disputed, 4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); bytes memory claimData7 = abi.encode(7, 7); - Claim postState_ = Claim.wrap(gameImpl.vm().step(claimData7, hex"", bytes32(0))); + Claim postState_ = Claim.wrap(gameProxy.vm().step(claimData7, hex"", bytes32(0))); (,,,, disputed,,) = gameProxy.claimData(5); gameProxy.attack{ value: _getRequiredBond(5) }(disputed, 5, _dummyClaim()); @@ -702,7 +938,7 @@ contract FaultDisputeGame_Step_Test is FaultDisputeGame_TestInit { gameProxy.attack{ value: _getRequiredBond(5) }(disputed, 5, claim5); (,,,, disputed,,) = gameProxy.claimData(6); gameProxy.defend{ value: _getRequiredBond(6) }(disputed, 6, _dummyClaim()); - Claim postState_ = Claim.wrap(gameImpl.vm().step(claimData5, hex"", bytes32(0))); + Claim postState_ = Claim.wrap(gameProxy.vm().step(claimData5, hex"", bytes32(0))); (,,,, disputed,,) = gameProxy.claimData(7); gameProxy.attack{ value: _getRequiredBond(7) }(disputed, 7, postState_); gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); @@ -730,7 +966,7 @@ contract FaultDisputeGame_Step_Test is FaultDisputeGame_TestInit { gameProxy.attack{ value: _getRequiredBond(4) }(disputed, 4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); bytes memory claimData7 = abi.encode(5, 5); - Claim postState_ = Claim.wrap(gameImpl.vm().step(claimData7, hex"", bytes32(0))); + Claim postState_ = Claim.wrap(gameProxy.vm().step(claimData7, hex"", bytes32(0))); (,,,, disputed,,) = gameProxy.claimData(5); gameProxy.attack{ value: _getRequiredBond(5) }(disputed, 5, postState_); @@ -766,7 +1002,7 @@ contract FaultDisputeGame_Step_Test is FaultDisputeGame_TestInit { bytes memory claimData7 = abi.encode(5, 5); Claim claim7 = Claim.wrap(keccak256(claimData7)); - Claim postState_ = Claim.wrap(gameImpl.vm().step(claimData7, hex"", bytes32(0))); + Claim postState_ = Claim.wrap(gameProxy.vm().step(claimData7, hex"", bytes32(0))); (,,,, disputed,,) = gameProxy.claimData(5); gameProxy.attack{ value: _getRequiredBond(5) }(disputed, 5, postState_); diff --git a/packages/contracts-bedrock/test/dispute/PermissionedDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/PermissionedDisputeGame.t.sol index 30c0e547a2981..4fcf6f5b99bda 100644 --- a/packages/contracts-bedrock/test/dispute/PermissionedDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/PermissionedDisputeGame.t.sol @@ -4,11 +4,14 @@ pragma solidity ^0.8.15; // Testing import { DisputeGameFactory_TestInit } from "test/dispute/DisputeGameFactory.t.sol"; import { AlphabetVM } from "test/mocks/AlphabetVM.sol"; + // Libraries import "src/dispute/lib/Types.sol"; import "src/dispute/lib/Errors.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces +import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; /// @title PermissionedDisputeGame_TestInit @@ -21,6 +24,9 @@ contract PermissionedDisputeGame_TestInit is DisputeGameFactory_TestInit { /// @notice Mock challenger key address internal constant CHALLENGER = address(0xfacadec); + /// @dev The initial bond for the game. + uint256 internal initBond; + /// @notice The implementation of the game. IPermissionedDisputeGame internal gameImpl; /// @notice The `Clone` proxy of the game. @@ -60,11 +66,8 @@ contract PermissionedDisputeGame_TestInit is DisputeGameFactory_TestInit { (address _impl, AlphabetVM _vm,) = setupPermissionedDisputeGame(_absolutePrestate, PROPOSER, CHALLENGER); gameImpl = IPermissionedDisputeGame(_impl); - // Register the game implementation with the factory. - disputeGameFactory.setImplementation(GAME_TYPE, gameImpl); - // Create a new game. - uint256 bondAmount = disputeGameFactory.initBonds(GAME_TYPE); + initBond = disputeGameFactory.initBonds(GAME_TYPE); vm.mockCall( address(anchorStateRegistry), abi.encodeCall(anchorStateRegistry.anchors, (GAME_TYPE)), @@ -72,7 +75,7 @@ contract PermissionedDisputeGame_TestInit is DisputeGameFactory_TestInit { ); vm.prank(PROPOSER, PROPOSER); gameProxy = IPermissionedDisputeGame( - payable(address(disputeGameFactory.create{ value: bondAmount }(GAME_TYPE, _rootClaim, extraData))) + payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, _rootClaim, extraData))) ); // Check immutables @@ -82,8 +85,13 @@ contract PermissionedDisputeGame_TestInit is DisputeGameFactory_TestInit { assertEq(gameProxy.absolutePrestate().raw(), _absolutePrestate.raw()); assertEq(gameProxy.maxGameDepth(), 2 ** 3); assertEq(gameProxy.splitDepth(), 2 ** 2); + assertEq(gameProxy.clockExtension().raw(), 3 hours); assertEq(gameProxy.maxClockDuration().raw(), 3.5 days); + assertEq(address(gameProxy.weth()), address(delayedWeth)); + assertEq(address(gameProxy.anchorStateRegistry()), address(anchorStateRegistry)); assertEq(address(gameProxy.vm()), address(_vm)); + assertEq(address(gameProxy.gameCreator()), PROPOSER); + assertEq(gameProxy.l2ChainId(), l2ChainId); // Label the proxy vm.label(address(gameProxy), "PermissionedDisputeGame_Clone"); @@ -124,6 +132,14 @@ contract PermissionedDisputeGame_TestInit is DisputeGameFactory_TestInit { fallback() external payable { } receive() external payable { } + + function copyBytes(bytes memory src, bytes memory dest) internal pure returns (bytes memory) { + uint256 byteCount = src.length < dest.length ? src.length : dest.length; + for (uint256 i = 0; i < byteCount; i++) { + dest[i] = src[i]; + } + return dest; + } } /// @title PermissionedDisputeGame_Version_Test @@ -138,13 +154,59 @@ contract PermissionedDisputeGame_Version_Test is PermissionedDisputeGame_TestIni /// @title PermissionedDisputeGame_Step_Test /// @notice Tests the `step` function of the `PermissionedDisputeGame` contract. contract PermissionedDisputeGame_Step_Test is PermissionedDisputeGame_TestInit { - /// @notice Tests that step works properly. - function test_step_succeeds() public { - // Give the test contract some ether + /// @notice Tests that step works properly for the challenger. + function test_step_fromChallenger_succeeds() public { + validateStepForActor(CHALLENGER); + } + + /// @notice Tests that step works properly for the proposer. + function test_step_fromProposer_succeeds() public { + validateStepForActor(PROPOSER); + } + + function validateStepForActor(address actor) internal { + vm.deal(actor, 1_000 ether); + vm.startPrank(actor, actor); + + // Set up and perform the step + setupGameForStep(); + performStep(); + assertEq(gameProxy.claimDataLen(), 9); + + // Resolve the game and check that the expected actor countered the root claim + resolveGame(); + assertEq(uint256(gameProxy.status()), uint256(GameStatus.CHALLENGER_WINS)); + assertEq(gameProxy.resolvedAt().raw(), block.timestamp); + (, address counteredBy,,,,,) = gameProxy.claimData(0); + assertEq(counteredBy, actor); + + vm.stopPrank(); + } + + /// @notice Tests that step reverts for unauthorized addresses. + function test_step_notAuthorized_reverts(address _unauthorized) internal { + vm.assume(_unauthorized != PROPOSER && _unauthorized != CHALLENGER); + vm.deal(_unauthorized, 1_000 ether); vm.deal(CHALLENGER, 1_000 ether); + // Set up for the step using an authorized actor vm.startPrank(CHALLENGER, CHALLENGER); + setupGameForStep(); + vm.stopPrank(); + + // Perform step with the unauthorized actor + vm.startPrank(_unauthorized, _unauthorized); + vm.expectRevert(BadAuth.selector); + performStep(); + + // Game should still be in progress, leaf claim should be missing + assertEq(uint256(gameProxy.status()), uint256(GameStatus.CHALLENGER_WINS)); + assertEq(gameProxy.claimDataLen(), 8); + vm.stopPrank(); + } + + function setupGameForStep() internal { // Make claims all the way down the tree. (,,,, Claim disputed,,) = gameProxy.claimData(0); gameProxy.attack{ value: _getRequiredBond(0) }(disputed, 0, _dummyClaim()); @@ -163,12 +225,16 @@ contract PermissionedDisputeGame_Step_Test is PermissionedDisputeGame_TestInit { (,,,, disputed,,) = gameProxy.claimData(7); gameProxy.attack{ value: _getRequiredBond(7) }(disputed, 7, _dummyClaim()); - // Verify game state before step + // Verify game state and add local data assertEq(uint256(gameProxy.status()), uint256(GameStatus.IN_PROGRESS)); - gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); + } + + function performStep() internal { gameProxy.step(8, true, absolutePrestateData, hex""); + } + function resolveGame() internal { vm.warp(block.timestamp + gameProxy.maxClockDuration().raw() + 1); gameProxy.resolveClaim(8, 0); gameProxy.resolveClaim(7, 0); @@ -181,11 +247,91 @@ contract PermissionedDisputeGame_Step_Test is PermissionedDisputeGame_TestInit { gameProxy.resolveClaim(0, 0); gameProxy.resolve(); + } +} - assertEq(uint256(gameProxy.status()), uint256(GameStatus.CHALLENGER_WINS)); - assertEq(gameProxy.resolvedAt().raw(), block.timestamp); - (, address counteredBy,,,,,) = gameProxy.claimData(0); - assertEq(counteredBy, CHALLENGER); +/// @title PermissionedDisputeGame_Initialize_Test +/// @notice Tests the initialization of the `PermissionedDisputeGame` contract. +contract PermissionedDisputeGame_Initialize_Test is PermissionedDisputeGame_TestInit { + /// @notice Tests that the game cannot be initialized with incorrect CWIA calldata length + /// caused by extraData of the wrong length + function test_initialize_wrongExtradataLength_reverts(uint256 _extraDataLen) public { + // The `DisputeGameFactory` will pack the root claim and the extra data into a single + // array, which is enforced to be at least 64 bytes long. + // We bound the upper end to 23.5KB to ensure that the minimal proxy never surpasses the + // contract size limit in this test, as CWIA proxies store the immutable args in their + // bytecode. + // [0 bytes, 31 bytes] u [33 bytes, 23.5 KB] + _extraDataLen = bound(_extraDataLen, 0, 23_500); + if (_extraDataLen == 32) { + _extraDataLen++; + } + bytes memory _extraData = new bytes(_extraDataLen); + + // Assign the first 32 bytes in `extraData` to a valid L2 block number passed the starting + // block. + (, uint256 startingL2Block) = gameProxy.startingOutputRoot(); + assembly { + mstore(add(_extraData, 0x20), add(startingL2Block, 1)) + } + + Claim claim = _dummyClaim(); + vm.prank(PROPOSER, PROPOSER); + vm.expectRevert(IFaultDisputeGame.BadExtraData.selector); + gameProxy = IPermissionedDisputeGame( + payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, _extraData))) + ); + } + + /// @notice Tests that the game cannot be initialized with incorrect CWIA calldata length + /// caused by additional immutable args data + function test_initialize_extraImmutableArgsBytes_reverts(uint256 _extraByteCount) public { + skipIfDevFeatureDisabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + (bytes memory correctArgs,,) = getPermissionedDisputeGameV2ImmutableArgs(absolutePrestate, PROPOSER, CHALLENGER); + + // We bound the upper end to 23.5KB to ensure that the minimal proxy never surpasses the + // contract size limit in this test, as CWIA proxies store the immutable args in their + // bytecode. + _extraByteCount = bound(_extraByteCount, 1, 23_500); + bytes memory immutableArgs = new bytes(_extraByteCount + correctArgs.length); + // Copy correct args into immutable args + copyBytes(correctArgs, immutableArgs); + + // Set up dispute game implementation with target immutableArgs + setupPermissionedDisputeGameV2(immutableArgs); + + Claim claim = _dummyClaim(); + vm.prank(PROPOSER, PROPOSER); + vm.expectRevert(IFaultDisputeGame.BadExtraData.selector); + gameProxy = IPermissionedDisputeGame( + payable( + address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, abi.encode(validL2BlockNumber))) + ) + ); + } + + /// @notice Tests that the game cannot be initialized with incorrect CWIA calldata length + /// caused by missing immutable args data + function test_initialize_missingImmutableArgsBytes_reverts(uint256 _truncatedByteCount) public { + skipIfDevFeatureDisabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + (bytes memory correctArgs,,) = getPermissionedDisputeGameV2ImmutableArgs(absolutePrestate, PROPOSER, CHALLENGER); + + _truncatedByteCount = (_truncatedByteCount % correctArgs.length) + 1; + bytes memory immutableArgs = new bytes(correctArgs.length - _truncatedByteCount); + // Copy correct args into immutable args + copyBytes(correctArgs, immutableArgs); + + // Set up dispute game implementation with target immutableArgs + setupPermissionedDisputeGameV2(immutableArgs); + + Claim claim = _dummyClaim(); + vm.prank(PROPOSER, PROPOSER); + vm.expectRevert(IFaultDisputeGame.BadExtraData.selector); + gameProxy = IPermissionedDisputeGame( + payable( + address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, abi.encode(validL2BlockNumber))) + ) + ); } } @@ -195,9 +341,16 @@ contract PermissionedDisputeGame_Step_Test is PermissionedDisputeGame_TestInit { contract PermissionedDisputeGame_Uncategorized_Test is PermissionedDisputeGame_TestInit { /// @notice Tests that the proposer can create a permissioned dispute game. function test_createGame_proposer_succeeds() public { - uint256 bondAmount = disputeGameFactory.initBonds(GAME_TYPE); vm.prank(PROPOSER, PROPOSER); - disputeGameFactory.create{ value: bondAmount }(GAME_TYPE, arbitaryRootClaim, abi.encode(validL2BlockNumber)); + disputeGameFactory.create{ value: initBond }(GAME_TYPE, arbitaryRootClaim, abi.encode(validL2BlockNumber)); + } + + /// @notice Tests that the permissioned game cannot be created by the challenger. + function test_createGame_challenger_reverts() public { + vm.deal(CHALLENGER, initBond); + vm.prank(CHALLENGER, CHALLENGER); + vm.expectRevert(BadAuth.selector); + disputeGameFactory.create{ value: initBond }(GAME_TYPE, arbitaryRootClaim, abi.encode(validL2BlockNumber)); } /// @notice Tests that the permissioned game cannot be created by any address other than the @@ -205,11 +358,10 @@ contract PermissionedDisputeGame_Uncategorized_Test is PermissionedDisputeGame_T function testFuzz_createGame_notProposer_reverts(address _p) public { vm.assume(_p != PROPOSER); - uint256 bondAmount = disputeGameFactory.initBonds(GAME_TYPE); - vm.deal(_p, bondAmount); + vm.deal(_p, initBond); vm.prank(_p, _p); vm.expectRevert(BadAuth.selector); - disputeGameFactory.create{ value: bondAmount }(GAME_TYPE, arbitaryRootClaim, abi.encode(validL2BlockNumber)); + disputeGameFactory.create{ value: initBond }(GAME_TYPE, arbitaryRootClaim, abi.encode(validL2BlockNumber)); } /// @notice Tests that the challenger can participate in a permissioned dispute game. diff --git a/packages/contracts-bedrock/test/dispute/v2/FaultDisputeGameV2.t.sol b/packages/contracts-bedrock/test/dispute/v2/FaultDisputeGameV2.t.sol deleted file mode 100644 index feab844563187..0000000000000 --- a/packages/contracts-bedrock/test/dispute/v2/FaultDisputeGameV2.t.sol +++ /dev/null @@ -1,3207 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -// Testing -import { Vm } from "forge-std/Vm.sol"; -import { DisputeGameFactory_TestInit } from "test/dispute/DisputeGameFactory.t.sol"; -import { AlphabetVM } from "test/mocks/AlphabetVM.sol"; -import { stdError } from "forge-std/StdError.sol"; - -// Scripts -import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; - -// Contracts -import { DisputeActor, HonestDisputeActor } from "test/actors/FaultDisputeActors.sol"; - -// Libraries -import { Types } from "src/libraries/Types.sol"; -import { Hashing } from "src/libraries/Hashing.sol"; -import { RLPWriter } from "src/libraries/rlp/RLPWriter.sol"; -import { LibClock } from "src/dispute/lib/LibUDT.sol"; -import { LibPosition } from "src/dispute/lib/LibPosition.sol"; -import "src/dispute/lib/Types.sol"; -import "src/dispute/lib/Errors.sol"; - -// Interfaces -import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; -import { IPreimageOracle } from "interfaces/dispute/IBigStepper.sol"; -import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; -import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; -import { IFaultDisputeGameV2 } from "interfaces/dispute/v2/IFaultDisputeGameV2.sol"; - -contract ClaimCreditReenter { - Vm internal immutable vm; - IFaultDisputeGameV2 internal immutable GAME; - uint256 public numCalls; - - constructor(IFaultDisputeGameV2 _gameProxy, Vm _vm) { - GAME = _gameProxy; - vm = _vm; - } - - function claimCredit(address _recipient) public { - numCalls += 1; - if (numCalls > 1) { - vm.expectRevert(NoCreditToClaim.selector); - } - GAME.claimCredit(_recipient); - } - - receive() external payable { - if (numCalls == 5) { - return; - } - claimCredit(address(this)); - } -} - -/// @notice Helper to change the VM status byte of a claim. -function _changeClaimStatus(Claim _claim, VMStatus _status) pure returns (Claim out_) { - assembly { - out_ := or(and(not(shl(248, 0xFF)), _claim), shl(248, _status)) - } -} - -/// @title BaseFaultDisputeGameV2_TestInit -/// @notice Base test initializer that can be used by other contracts outside of this test suite. -contract BaseFaultDisputeGameV2_TestInit is DisputeGameFactory_TestInit { - /// @dev The type of the game being tested. - GameType internal immutable GAME_TYPE = GameTypes.CANNON; - - /// @dev The initial bond for the game. - uint256 internal initBond; - - /// @dev The implementation of the game. - IFaultDisputeGameV2 internal gameImpl; - /// @dev The `Clone` proxy of the game. - IFaultDisputeGameV2 internal gameProxy; - - /// @dev The extra data passed to the game for initialization. - bytes internal extraData; - - event Move(uint256 indexed parentIndex, Claim indexed pivot, address indexed claimant); - event GameClosed(BondDistributionMode bondDistributionMode); - - event ReceiveETH(uint256 amount); - - function init(Claim rootClaim, Claim absolutePrestate, uint256 l2BlockNumber) public { - // Set the time to a realistic date. - if (!isForkTest()) { - vm.warp(1690906994); - } - - // Set the extra data for the game creation - extraData = abi.encode(l2BlockNumber); - - (address _impl, AlphabetVM _vm,) = setupFaultDisputeGameV2(absolutePrestate); - gameImpl = IFaultDisputeGameV2(_impl); - - // Set the init bond for the given game type. - initBond = disputeGameFactory.initBonds(GAME_TYPE); - - // Warp ahead of the game retirement timestamp if needed. - if (block.timestamp <= anchorStateRegistry.retirementTimestamp()) { - vm.warp(anchorStateRegistry.retirementTimestamp() + 1); - } - - // Create a new game. - gameProxy = IFaultDisputeGameV2( - payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, rootClaim, extraData))) - ); - - // Check immutables - assertEq(gameProxy.gameType().raw(), GAME_TYPE.raw()); - assertEq(gameProxy.absolutePrestate().raw(), absolutePrestate.raw()); - assertEq(gameProxy.maxGameDepth(), 2 ** 3); - assertEq(gameProxy.splitDepth(), 2 ** 2); - assertEq(gameProxy.clockExtension().raw(), 3 hours); - assertEq(gameProxy.maxClockDuration().raw(), 3.5 days); - assertEq(address(gameProxy.weth()), address(delayedWeth)); - assertEq(address(gameProxy.anchorStateRegistry()), address(anchorStateRegistry)); - assertEq(address(gameProxy.vm()), address(_vm)); - assertEq(address(gameProxy.gameCreator()), address(this)); - assertEq(gameProxy.l2ChainId(), l2ChainId); - - // Label the proxy - vm.label(address(gameProxy), "FaultDisputeGame_Clone"); - } - - fallback() external payable { } - - receive() external payable { } - - function copyBytes(bytes memory src, bytes memory dest) internal pure returns (bytes memory) { - uint256 byteCount = src.length < dest.length ? src.length : dest.length; - for (uint256 i = 0; i < byteCount; i++) { - dest[i] = src[i]; - } - return dest; - } -} - -/// @title FaultDisputeGameV2_TestInit -/// @notice Reusable test initialization for `FaultDisputeGame` tests. -contract FaultDisputeGameV2_TestInit is BaseFaultDisputeGameV2_TestInit { - /// @dev The root claim of the game. - Claim internal ROOT_CLAIM; - /// @dev An arbitrary root claim for testing. - Claim internal arbitaryRootClaim = Claim.wrap(bytes32(uint256(123))); - - /// @dev The preimage of the absolute prestate claim - bytes internal absolutePrestateData; - /// @dev The absolute prestate of the trace. - Claim internal absolutePrestate; - /// @dev A valid l2BlockNumber that comes after the current anchor root block. - uint256 internal validL2BlockNumber; - - function setUp() public virtual override { - absolutePrestateData = abi.encode(0); - absolutePrestate = _changeClaimStatus(Claim.wrap(keccak256(absolutePrestateData)), VMStatuses.UNFINISHED); - - super.setUp(); - - // Get the actual anchor roots - (Hash root, uint256 l2Bn) = anchorStateRegistry.getAnchorRoot(); - validL2BlockNumber = l2Bn + 1; - - ROOT_CLAIM = Claim.wrap(Hash.unwrap(root)); - - super.init({ rootClaim: ROOT_CLAIM, absolutePrestate: absolutePrestate, l2BlockNumber: validL2BlockNumber }); - } - - /// @notice Helper to generate a mock RLP encoded header (with only a real block number) & an - /// 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 - 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_ = Types.OutputRootProof({ - version: 0, - stateRoot: _storageRoot, - messagePasserStorageRoot: _withdrawalRoot, - latestBlockhash: keccak256(rlp_) - }); - root_ = Hashing.hashOutputRootProof(proof_); - } - - /// @notice Helper to get the required bond for the given claim index. - function _getRequiredBond(uint256 _claimIndex) internal view returns (uint256 bond_) { - (,,,,, Position parent,) = gameProxy.claimData(_claimIndex); - Position pos = parent.move(true); - bond_ = gameProxy.getRequiredBond(pos); - } - - /// @notice Helper to return a pseudo-random claim - function _dummyClaim() internal view returns (Claim) { - return Claim.wrap(keccak256(abi.encode(gasleft()))); - } - - /// @notice Helper to get the localized key for an identifier in the context of the game proxy. - function _getKey(uint256 _ident, bytes32 _localContext) internal view returns (bytes32) { - bytes32 h = keccak256(abi.encode(_ident | (1 << 248), address(gameProxy), _localContext)); - return bytes32((uint256(h) & ~uint256(0xFF << 248)) | (1 << 248)); - } -} - -/// @title FaultDisputeGame_Version_Test -/// @notice Tests the `version` function of the `FaultDisputeGame` contract. -contract FaultDisputeGameV2_Version_Test is FaultDisputeGameV2_TestInit { - /// @notice Tests that the game's version function returns a string. - function test_version_works() public view { - assertTrue(bytes(gameProxy.version()).length > 0); - } -} - -/// @title FaultDisputeGame_Constructor_Test -/// @notice Tests the constructor of the `FaultDisputeGame` contract. -contract FaultDisputeGameV2_Constructor_Test is FaultDisputeGameV2_TestInit { - /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when the - /// `MAX_GAME_DEPTH` parameter is greater than `LibPosition.MAX_POSITION_BITLEN - 1`. - function testFuzz_constructor_maxDepthTooLarge_reverts(uint256 _maxGameDepth) public { - _maxGameDepth = bound(_maxGameDepth, LibPosition.MAX_POSITION_BITLEN, type(uint256).max - 1); - vm.expectRevert(MaxDepthTooLarge.selector); - DeployUtils.create1({ - _name: "FaultDisputeGameV2", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IFaultDisputeGameV2.__constructor__, - ( - IFaultDisputeGameV2.GameConstructorParams({ - gameType: GAME_TYPE, - maxGameDepth: _maxGameDepth, - splitDepth: _maxGameDepth + 1, - clockExtension: Duration.wrap(3 hours), - maxClockDuration: Duration.wrap(3.5 days) - }) - ) - ) - ) - }); - } - - /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when the `_splitDepth` - /// parameter is greater than or equal to the `MAX_GAME_DEPTH` - function testFuzz_constructor_invalidSplitDepth_reverts(uint256 _splitDepth) public { - uint256 maxGameDepth = 2 ** 3; - _splitDepth = bound(_splitDepth, maxGameDepth - 1, type(uint256).max); - vm.expectRevert(InvalidSplitDepth.selector); - DeployUtils.create1({ - _name: "FaultDisputeGameV2", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IFaultDisputeGameV2.__constructor__, - ( - IFaultDisputeGameV2.GameConstructorParams({ - gameType: GAME_TYPE, - maxGameDepth: maxGameDepth, - splitDepth: _splitDepth, - clockExtension: Duration.wrap(3 hours), - maxClockDuration: Duration.wrap(3.5 days) - }) - ) - ) - ) - }); - } - - /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when the `_splitDepth` - /// parameter is less than the minimum split depth (currently 2). - function testFuzz_constructor_lowSplitDepth_reverts(uint256 _splitDepth) public { - uint256 minSplitDepth = 2; - _splitDepth = bound(_splitDepth, 0, minSplitDepth - 1); - vm.expectRevert(InvalidSplitDepth.selector); - DeployUtils.create1({ - _name: "FaultDisputeGameV2", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IFaultDisputeGameV2.__constructor__, - ( - IFaultDisputeGameV2.GameConstructorParams({ - gameType: GAME_TYPE, - maxGameDepth: 2 ** 3, - splitDepth: _splitDepth, - clockExtension: Duration.wrap(3 hours), - maxClockDuration: Duration.wrap(3.5 days) - }) - ) - ) - ) - }); - } - - /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when clock - /// extension * 2 is greater than the max clock duration. - function testFuzz_constructor_clockExtensionTooLong_reverts( - uint64 _maxClockDuration, - uint64 _clockExtension - ) - public - { - // Force the clock extension * 2 to be greater than the max clock duration, but keep things - // within bounds of the uint64 type. - _maxClockDuration = uint64(bound(_maxClockDuration, 0, type(uint64).max / 2 - 1)); - _clockExtension = uint64(bound(_clockExtension, _maxClockDuration / 2 + 1, type(uint64).max / 2)); - - vm.expectRevert(InvalidClockExtension.selector); - DeployUtils.create1({ - _name: "FaultDisputeGameV2", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IFaultDisputeGameV2.__constructor__, - ( - IFaultDisputeGameV2.GameConstructorParams({ - gameType: GAME_TYPE, - maxGameDepth: 16, - splitDepth: 8, - clockExtension: Duration.wrap(_clockExtension), - maxClockDuration: Duration.wrap(_maxClockDuration) - }) - ) - ) - ) - }); - } - - /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when the `_gameType` - /// parameter is set to the reserved `type(uint32).max` game type. - function test_constructor_reservedGameType_reverts() public { - vm.expectRevert(ReservedGameType.selector); - DeployUtils.create1({ - _name: "FaultDisputeGameV2", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IFaultDisputeGameV2.__constructor__, - ( - IFaultDisputeGameV2.GameConstructorParams({ - gameType: GameType.wrap(type(uint32).max), - maxGameDepth: 16, - splitDepth: 8, - clockExtension: Duration.wrap(3 hours), - maxClockDuration: Duration.wrap(3.5 days) - }) - ) - ) - ) - }); - } -} - -/// @title FaultDisputeGame_Initialize_Test -/// @notice Tests the initialization of the `FaultDisputeGame` contract. -contract FaultDisputeGameV2_Initialize_Test is FaultDisputeGameV2_TestInit { - /// @notice Tests that the game cannot be initialized with an output root that commits to <= - /// the configured starting block number - function testFuzz_initialize_cannotProposeGenesis_reverts(uint256 _blockNumber) public { - (, uint256 startingL2Block) = gameProxy.startingOutputRoot(); - _blockNumber = bound(_blockNumber, 0, startingL2Block); - - Claim claim = _dummyClaim(); - vm.expectRevert(abi.encodeWithSelector(UnexpectedRootClaim.selector, claim)); - gameProxy = IFaultDisputeGameV2( - payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, abi.encode(_blockNumber)))) - ); - } - - /// @notice Tests that the proxy receives ETH from the dispute game factory. - function test_initialize_receivesETH_succeeds() public { - uint256 _value = disputeGameFactory.initBonds(GAME_TYPE); - vm.deal(address(this), _value); - - assertEq(address(gameProxy).balance, 0); - gameProxy = IFaultDisputeGameV2( - payable( - address( - disputeGameFactory.create{ value: _value }( - GAME_TYPE, arbitaryRootClaim, abi.encode(validL2BlockNumber) - ) - ) - ) - ); - assertEq(address(gameProxy).balance, 0); - assertEq(delayedWeth.balanceOf(address(gameProxy)), _value); - } - - /// @notice Tests that the game cannot be initialized with incorrect CWIA calldata length - /// caused by extraData of the wrong length - function test_initialize_wrongExtradataLength_reverts(uint256 _extraDataLen) public { - // The `DisputeGameFactory` will pack the root claim and the extra data into a single - // array, which is enforced to be at least 64 bytes long. - // We bound the upper end to 23.5KB to ensure that the minimal proxy never surpasses the - // contract size limit in this test, as CWIA proxies store the immutable args in their - // bytecode. - // [0 bytes, 31 bytes] u [33 bytes, 23.5 KB] - _extraDataLen = bound(_extraDataLen, 0, 23_500); - if (_extraDataLen == 32) { - _extraDataLen++; - } - bytes memory _extraData = new bytes(_extraDataLen); - - // Assign the first 32 bytes in `extraData` to a valid L2 block number passed the starting - // block. - (, uint256 startingL2Block) = gameProxy.startingOutputRoot(); - assembly { - mstore(add(_extraData, 0x20), add(startingL2Block, 1)) - } - - Claim claim = _dummyClaim(); - vm.expectRevert(IFaultDisputeGameV2.BadExtraData.selector); - gameProxy = IFaultDisputeGameV2( - payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, _extraData))) - ); - } - - /// @notice Tests that the game cannot be initialized with incorrect CWIA calldata length - /// caused by additional immutable args data - function test_initialize_extraImmutableArgsBytes_reverts(uint256 _extraByteCount) public { - (bytes memory correctArgs,,) = getFaultDisputeGameV2ImmutableArgs(absolutePrestate); - - // We bound the upper end to 23.5KB to ensure that the minimal proxy never surpasses the - // contract size limit in this test, as CWIA proxies store the immutable args in their - // bytecode. - _extraByteCount = bound(_extraByteCount, 1, 23_500); - bytes memory immutableArgs = new bytes(_extraByteCount + correctArgs.length); - // Copy correct args into immutable args - copyBytes(correctArgs, immutableArgs); - - // Set up dispute game implementation with target immutableArgs - setupFaultDisputeGameV2(immutableArgs); - - Claim claim = _dummyClaim(); - vm.expectRevert(IFaultDisputeGameV2.BadExtraData.selector); - gameProxy = IFaultDisputeGameV2( - payable( - address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, abi.encode(validL2BlockNumber))) - ) - ); - } - - /// @notice Tests that the game cannot be initialized with incorrect CWIA calldata length - /// caused by missing immutable args data - function test_initialize_missingImmutableArgsBytes_reverts(uint256 _truncatedByteCount) public { - (bytes memory correctArgs,,) = getFaultDisputeGameV2ImmutableArgs(absolutePrestate); - - _truncatedByteCount = (_truncatedByteCount % correctArgs.length) + 1; - bytes memory immutableArgs = new bytes(correctArgs.length - _truncatedByteCount); - // Copy correct args into immutable args - copyBytes(correctArgs, immutableArgs); - - // Set up dispute game implementation with target immutableArgs - setupFaultDisputeGameV2(immutableArgs); - - Claim claim = _dummyClaim(); - vm.expectRevert(IFaultDisputeGameV2.BadExtraData.selector); - gameProxy = IFaultDisputeGameV2( - payable( - address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, abi.encode(validL2BlockNumber))) - ) - ); - } - - /// @notice Tests that the game is initialized with the correct data. - function test_initialize_correctData_succeeds() public view { - // Assert that the root claim is initialized correctly. - ( - uint32 parentIndex, - address counteredBy, - address claimant, - uint128 bond, - Claim claim, - Position position, - Clock clock - ) = gameProxy.claimData(0); - assertEq(parentIndex, type(uint32).max); - assertEq(counteredBy, address(0)); - assertEq(claimant, address(this)); - assertEq(bond, initBond); - assertEq(claim.raw(), ROOT_CLAIM.raw()); - assertEq(position.raw(), 1); - assertEq(clock.raw(), LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))).raw()); - - // Assert that the `createdAt` timestamp is correct. - assertEq(gameProxy.createdAt().raw(), block.timestamp); - - // Assert that the blockhash provided is correct. - assertEq(gameProxy.l1Head().raw(), blockhash(block.number - 1)); - } - - /// @notice Tests that the game cannot be initialized when the anchor root is not found. - function test_initialize_anchorRootNotFound_reverts() public { - // Mock the AnchorStateRegistry to return a zero root. - vm.mockCall( - address(anchorStateRegistry), - abi.encodeCall(IAnchorStateRegistry.getAnchorRoot, ()), - abi.encode(Hash.wrap(bytes32(0)), 0) - ); - - // Creation should fail. - vm.expectRevert(AnchorRootNotFound.selector); - gameProxy = IFaultDisputeGameV2( - payable( - address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, _dummyClaim(), new bytes(uint256(32)))) - ) - ); - } - - /// @notice Tests that the game cannot be initialized twice. - function test_initialize_onlyOnce_succeeds() public { - vm.expectRevert(AlreadyInitialized.selector); - gameProxy.initialize(); - } - - /// @notice Tests that initialization reverts when oracle challenge period is too large. - /// @dev V2 validates oracle challenge period during initialize(), not constructor - function testFuzz_initialize_oracleChallengePeriodTooLarge_reverts(uint256 _challengePeriod) public { - // Bound to values larger than uint64.max - _challengePeriod = bound(_challengePeriod, uint256(type(uint64).max) + 1, type(uint256).max); - - // Get the current AlphabetVM from the setup - (, AlphabetVM vm_,) = setupFaultDisputeGameV2(absolutePrestate); - - // Mock the VM's oracle to return invalid challenge period - vm.mockCall( - address(vm_.oracle()), abi.encodeCall(IPreimageOracle.challengePeriod, ()), abi.encode(_challengePeriod) - ); - - // Expect the initialize call to revert with InvalidChallengePeriod - vm.expectRevert(InvalidChallengePeriod.selector); - - // Create game via factory - initialize() is called automatically and should revert - gameProxy = IFaultDisputeGameV2( - payable( - address( - disputeGameFactory.create{ value: initBond }( - GAME_TYPE, _dummyClaim(), abi.encode(validL2BlockNumber) - ) - ) - ) - ); - } -} - -/// @title FaultDisputeGame_Step_Test -/// @notice Tests the step functionality of the `FaultDisputeGame` contract. -contract FaultDisputeGameV2_Step_Test is FaultDisputeGameV2_TestInit { - /// @notice Tests that a claim cannot be stepped against twice. - function test_step_duplicateStep_reverts() public { - // Give the test contract some ether - vm.deal(address(this), 1000 ether); - - // Make claims all the way down the tree. - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: _getRequiredBond(0) }(disputed, 0, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.attack{ value: _getRequiredBond(1) }(disputed, 1, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(2); - gameProxy.attack{ value: _getRequiredBond(2) }(disputed, 2, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(3); - gameProxy.attack{ value: _getRequiredBond(3) }(disputed, 3, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(4); - gameProxy.attack{ value: _getRequiredBond(4) }(disputed, 4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); - (,,,, disputed,,) = gameProxy.claimData(5); - gameProxy.attack{ value: _getRequiredBond(5) }(disputed, 5, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(6); - gameProxy.attack{ value: _getRequiredBond(6) }(disputed, 6, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(7); - gameProxy.attack{ value: _getRequiredBond(7) }(disputed, 7, _dummyClaim()); - gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); - gameProxy.step(8, true, absolutePrestateData, hex""); - - vm.expectRevert(DuplicateStep.selector); - gameProxy.step(8, true, absolutePrestateData, hex""); - } - - /// @notice Tests that successfully step with true attacking claim when there is a true defend - /// claim(claim5) in the middle of the dispute game. - function test_stepAttackDummyClaim_defendTrueClaimInTheMiddle_succeeds() public { - // Give the test contract some ether - vm.deal(address(this), 1000 ether); - - // Make claims all the way down the tree. - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: _getRequiredBond(0) }(disputed, 0, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.attack{ value: _getRequiredBond(1) }(disputed, 1, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(2); - gameProxy.attack{ value: _getRequiredBond(2) }(disputed, 2, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(3); - gameProxy.attack{ value: _getRequiredBond(3) }(disputed, 3, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(4); - gameProxy.attack{ value: _getRequiredBond(4) }(disputed, 4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); - bytes memory claimData5 = abi.encode(5, 5); - Claim claim5 = Claim.wrap(keccak256(claimData5)); - (,,,, disputed,,) = gameProxy.claimData(5); - gameProxy.attack{ value: _getRequiredBond(5) }(disputed, 5, claim5); - (,,,, disputed,,) = gameProxy.claimData(6); - gameProxy.defend{ value: _getRequiredBond(6) }(disputed, 6, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(7); - gameProxy.attack{ value: _getRequiredBond(7) }(disputed, 7, _dummyClaim()); - gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); - gameProxy.step(8, true, claimData5, hex""); - } - - /// @notice Tests that successfully step with true defend claim when there is a true defend - /// claim(claim7) in the middle of the dispute game. - function test_stepDefendDummyClaim_defendTrueClaimInTheMiddle_succeeds() public { - // Give the test contract some ether - vm.deal(address(this), 1000 ether); - - // Make claims all the way down the tree. - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: _getRequiredBond(0) }(disputed, 0, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.attack{ value: _getRequiredBond(1) }(disputed, 1, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(2); - gameProxy.attack{ value: _getRequiredBond(2) }(disputed, 2, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(3); - gameProxy.attack{ value: _getRequiredBond(3) }(disputed, 3, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(4); - gameProxy.attack{ value: _getRequiredBond(4) }(disputed, 4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); - - bytes memory claimData7 = abi.encode(7, 7); - Claim postState_ = Claim.wrap(gameProxy.vm().step(claimData7, hex"", bytes32(0))); - - (,,,, disputed,,) = gameProxy.claimData(5); - gameProxy.attack{ value: _getRequiredBond(5) }(disputed, 5, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(6); - gameProxy.defend{ value: _getRequiredBond(6) }(disputed, 6, postState_); - (,,,, disputed,,) = gameProxy.claimData(7); - - gameProxy.attack{ value: _getRequiredBond(7) }(disputed, 7, Claim.wrap(keccak256(claimData7))); - gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); - gameProxy.step(8, false, claimData7, hex""); - } - - /// @notice Tests that step reverts with false attacking claim when there is a true defend - /// claim(claim5) in the middle of the dispute game. - function test_stepAttackTrueClaim_defendTrueClaimInTheMiddle_reverts() public { - // Give the test contract some ether - vm.deal(address(this), 1000 ether); - - // Make claims all the way down the tree. - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: _getRequiredBond(0) }(disputed, 0, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.attack{ value: _getRequiredBond(1) }(disputed, 1, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(2); - gameProxy.attack{ value: _getRequiredBond(2) }(disputed, 2, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(3); - gameProxy.attack{ value: _getRequiredBond(3) }(disputed, 3, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(4); - gameProxy.attack{ value: _getRequiredBond(4) }(disputed, 4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); - bytes memory claimData5 = abi.encode(5, 5); - Claim claim5 = Claim.wrap(keccak256(claimData5)); - (,,,, disputed,,) = gameProxy.claimData(5); - gameProxy.attack{ value: _getRequiredBond(5) }(disputed, 5, claim5); - (,,,, disputed,,) = gameProxy.claimData(6); - gameProxy.defend{ value: _getRequiredBond(6) }(disputed, 6, _dummyClaim()); - Claim postState_ = Claim.wrap(gameProxy.vm().step(claimData5, hex"", bytes32(0))); - (,,,, disputed,,) = gameProxy.claimData(7); - gameProxy.attack{ value: _getRequiredBond(7) }(disputed, 7, postState_); - gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); - - vm.expectRevert(ValidStep.selector); - gameProxy.step(8, true, claimData5, hex""); - } - - /// @notice Tests that step reverts with false defending claim when there is a true defend - /// claim(postState_) in the middle of the dispute game. - function test_stepDefendDummyClaim_defendTrueClaimInTheMiddle_reverts() public { - // Give the test contract some ether - vm.deal(address(this), 1000 ether); - - // Make claims all the way down the tree. - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: _getRequiredBond(0) }(disputed, 0, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.attack{ value: _getRequiredBond(1) }(disputed, 1, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(2); - gameProxy.attack{ value: _getRequiredBond(2) }(disputed, 2, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(3); - gameProxy.attack{ value: _getRequiredBond(3) }(disputed, 3, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(4); - gameProxy.attack{ value: _getRequiredBond(4) }(disputed, 4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); - - bytes memory claimData7 = abi.encode(5, 5); - Claim postState_ = Claim.wrap(gameProxy.vm().step(claimData7, hex"", bytes32(0))); - - (,,,, disputed,,) = gameProxy.claimData(5); - gameProxy.attack{ value: _getRequiredBond(5) }(disputed, 5, postState_); - (,,,, disputed,,) = gameProxy.claimData(6); - gameProxy.defend{ value: _getRequiredBond(6) }(disputed, 6, _dummyClaim()); - - bytes memory _dummyClaimData = abi.encode(gasleft(), gasleft()); - Claim dummyClaim7 = Claim.wrap(keccak256(_dummyClaimData)); - (,,,, disputed,,) = gameProxy.claimData(7); - gameProxy.attack{ value: _getRequiredBond(7) }(disputed, 7, dummyClaim7); - gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); - vm.expectRevert(ValidStep.selector); - gameProxy.step(8, false, _dummyClaimData, hex""); - } - - /// @notice Tests that step reverts with true defending claim when there is a true defend - /// claim(postState_) in the middle of the dispute game. - function test_stepDefendTrueClaim_defendTrueClaimInTheMiddle_reverts() public { - // Give the test contract some ether - vm.deal(address(this), 1000 ether); - - // Make claims all the way down the tree. - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: _getRequiredBond(0) }(disputed, 0, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.attack{ value: _getRequiredBond(1) }(disputed, 1, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(2); - gameProxy.attack{ value: _getRequiredBond(2) }(disputed, 2, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(3); - gameProxy.attack{ value: _getRequiredBond(3) }(disputed, 3, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(4); - gameProxy.attack{ value: _getRequiredBond(4) }(disputed, 4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); - - bytes memory claimData7 = abi.encode(5, 5); - Claim claim7 = Claim.wrap(keccak256(claimData7)); - Claim postState_ = Claim.wrap(gameProxy.vm().step(claimData7, hex"", bytes32(0))); - - (,,,, disputed,,) = gameProxy.claimData(5); - gameProxy.attack{ value: _getRequiredBond(5) }(disputed, 5, postState_); - (,,,, disputed,,) = gameProxy.claimData(6); - gameProxy.defend{ value: _getRequiredBond(6) }(disputed, 6, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(7); - gameProxy.attack{ value: _getRequiredBond(7) }(disputed, 7, claim7); - gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); - - vm.expectRevert(ValidStep.selector); - gameProxy.step(8, false, claimData7, hex""); - } -} - -/// @title FaultDisputeGame_Move_Test -/// @notice Tests the move functionality of the `FaultDisputeGame` contract. -contract FaultDisputeGameV2_Move_Test is FaultDisputeGameV2_TestInit { - /// @notice Tests that a move while the game status is not `IN_PROGRESS` causes the call to - /// revert with the `GameNotInProgress` error - function test_move_gameNotInProgress_reverts() public { - uint256 chalWins = uint256(GameStatus.CHALLENGER_WINS); - - // Replace the game status in storage. It exists in slot 0 at offset 16. - uint256 slot = uint256(vm.load(address(gameProxy), bytes32(0))); - uint256 offset = 16 << 3; - uint256 mask = 0xFF << offset; - // Replace the byte in the slot value with the challenger wins status. - slot = (slot & ~mask) | (chalWins << offset); - vm.store(address(gameProxy), bytes32(0), bytes32(slot)); - - // Ensure that the game status was properly updated. - GameStatus status = gameProxy.status(); - assertEq(uint256(status), chalWins); - - (,,,, Claim root,,) = gameProxy.claimData(0); - // Attempt to make a move. Should revert. - vm.expectRevert(GameNotInProgress.selector); - gameProxy.attack(root, 0, Claim.wrap(0)); - } - - /// @notice Tests that an attempt to defend the root claim reverts with the - /// `CannotDefendRootClaim` error. - function test_move_defendRoot_reverts() public { - (,,,, Claim root,,) = gameProxy.claimData(0); - vm.expectRevert(CannotDefendRootClaim.selector); - gameProxy.defend(root, 0, _dummyClaim()); - } - - /// @notice Tests that an attempt to move against a claim that does not exist reverts with the - /// `ParentDoesNotExist` error. - function test_move_nonExistentParent_reverts() public { - Claim claim = _dummyClaim(); - - // Expect an out of bounds revert for an attack - vm.expectRevert(stdError.indexOOBError); - gameProxy.attack(_dummyClaim(), 1, claim); - - // Expect an out of bounds revert for a defense - vm.expectRevert(stdError.indexOOBError); - gameProxy.defend(_dummyClaim(), 1, claim); - } - - /// @notice Tests that an attempt to move at the maximum game depth reverts with the - /// `GameDepthExceeded` error. - function test_move_gameDepthExceeded_reverts() public { - Claim claim = _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC); - - uint256 maxDepth = gameProxy.maxGameDepth(); - - for (uint256 i = 0; i <= maxDepth; i++) { - (,,,, Claim disputed,,) = gameProxy.claimData(i); - // At the max game depth, the `_move` function should revert with - // the `GameDepthExceeded` error. - if (i == maxDepth) { - vm.expectRevert(GameDepthExceeded.selector); - gameProxy.attack{ value: 100 ether }(disputed, i, claim); - } else { - gameProxy.attack{ value: _getRequiredBond(i) }(disputed, i, claim); - } - } - } - - /// @notice Tests that a move made after the clock time has exceeded reverts with the - /// `ClockTimeExceeded` error. - function test_move_clockTimeExceeded_reverts() public { - // Warp ahead past the clock time for the first move (3 1/2 days) - vm.warp(block.timestamp + 3 days + 12 hours + 1); - uint256 bond = _getRequiredBond(0); - (,,,, Claim disputed,,) = gameProxy.claimData(0); - vm.expectRevert(ClockTimeExceeded.selector); - gameProxy.attack{ value: bond }(disputed, 0, _dummyClaim()); - } - - /// @notice Static unit test for the correctness of the chess clock incrementation. - function test_move_clockCorrectness_succeeds() public { - (,,,,,, Clock clock) = gameProxy.claimData(0); - assertEq(clock.raw(), LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))).raw()); - - Claim claim = _dummyClaim(); - - vm.warp(block.timestamp + 15); - uint256 bond = _getRequiredBond(0); - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: bond }(disputed, 0, claim); - (,,,,,, clock) = gameProxy.claimData(1); - assertEq(clock.raw(), LibClock.wrap(Duration.wrap(15), Timestamp.wrap(uint64(block.timestamp))).raw()); - - vm.warp(block.timestamp + 10); - bond = _getRequiredBond(1); - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.attack{ value: bond }(disputed, 1, claim); - (,,,,,, clock) = gameProxy.claimData(2); - assertEq(clock.raw(), LibClock.wrap(Duration.wrap(10), Timestamp.wrap(uint64(block.timestamp))).raw()); - - // We are at the split depth, so we need to set the status byte of the claim for the next - // move. - claim = _changeClaimStatus(claim, VMStatuses.PANIC); - - vm.warp(block.timestamp + 10); - bond = _getRequiredBond(2); - (,,,, disputed,,) = gameProxy.claimData(2); - gameProxy.attack{ value: bond }(disputed, 2, claim); - (,,,,,, clock) = gameProxy.claimData(3); - assertEq(clock.raw(), LibClock.wrap(Duration.wrap(25), Timestamp.wrap(uint64(block.timestamp))).raw()); - - vm.warp(block.timestamp + 10); - bond = _getRequiredBond(3); - (,,,, disputed,,) = gameProxy.claimData(3); - gameProxy.attack{ value: bond }(disputed, 3, claim); - (,,,,,, clock) = gameProxy.claimData(4); - assertEq(clock.raw(), LibClock.wrap(Duration.wrap(20), Timestamp.wrap(uint64(block.timestamp))).raw()); - } - - /// @notice Tests that the standard clock extension is triggered for a move that is not the - /// split depth or the max game depth. - function test_move_standardClockExtension_succeeds() public { - (,,,,,, Clock clock) = gameProxy.claimData(0); - assertEq(clock.raw(), LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))).raw()); - - uint256 bond; - Claim disputed; - Claim claim = _dummyClaim(); - uint256 splitDepth = gameProxy.splitDepth(); - uint64 halfGameDuration = gameProxy.maxClockDuration().raw(); - uint64 clockExtension = gameProxy.clockExtension().raw(); - - // Warp ahead so that the next move will trigger a clock extension. We warp to the very - // first timestamp where a clock extension should be triggered. - vm.warp(block.timestamp + halfGameDuration - clockExtension + 1 seconds); - - // Execute a move that should cause a clock extension. - bond = _getRequiredBond(0); - (,,,, disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: bond }(disputed, 0, claim); - (,,,,,, clock) = gameProxy.claimData(1); - - // The clock should have been pushed back to the clock extension time. - assertEq(clock.duration().raw(), halfGameDuration - clockExtension); - - // Warp ahead again so that clock extensions will also trigger for the other team. Here we - // only warp to the clockExtension time because we'll be warping ahead by one second during - // each additional move. - vm.warp(block.timestamp + halfGameDuration - clockExtension); - - // Work our way down to the split depth. - for (uint256 i = 1; i < splitDepth - 2; i++) { - // Warp ahead by one second so that the next move will trigger a clock extension. - vm.warp(block.timestamp + 1 seconds); - - // Execute a move that should cause a clock extension. - bond = _getRequiredBond(i); - (,,,, disputed,,) = gameProxy.claimData(i); - gameProxy.attack{ value: bond }(disputed, i, claim); - (,,,,,, clock) = gameProxy.claimData(i + 1); - - // The clock should have been pushed back to the clock extension time. - assertEq(clock.duration().raw(), halfGameDuration - clockExtension); - } - } - - function test_move_splitDepthClockExtension_succeeds() public { - (,,,,,, Clock clock) = gameProxy.claimData(0); - assertEq(clock.raw(), LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))).raw()); - - uint256 bond; - Claim disputed; - Claim claim = _dummyClaim(); - uint256 splitDepth = gameProxy.splitDepth(); - uint64 halfGameDuration = gameProxy.maxClockDuration().raw(); - uint64 clockExtension = gameProxy.clockExtension().raw(); - - // Work our way down to the split depth without moving ahead in time, we don't care about - // the exact clock here, just don't want take the clock below the clock extension time that - // we're trying to test here. - for (uint256 i = 0; i < splitDepth - 2; i++) { - bond = _getRequiredBond(i); - (,,,, disputed,,) = gameProxy.claimData(i); - gameProxy.attack{ value: bond }(disputed, i, claim); - } - - // Warp ahead to the very first timestamp where a clock extension should be triggered. - vm.warp(block.timestamp + halfGameDuration - clockExtension * 2 + 1 seconds); - - // Execute a move that should cause a clock extension. - bond = _getRequiredBond(splitDepth - 2); - (,,,, disputed,,) = gameProxy.claimData(splitDepth - 2); - gameProxy.attack{ value: bond }(disputed, splitDepth - 2, claim); - (,,,,,, clock) = gameProxy.claimData(splitDepth - 1); - - // The clock should have been pushed back to the clock extension time. - assertEq(clock.duration().raw(), halfGameDuration - clockExtension * 2); - } - - function test_move_maxGameDepthClockExtension_succeeds() public { - (,,,,,, Clock clock) = gameProxy.claimData(0); - assertEq(clock.raw(), LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))).raw()); - - uint256 bond; - Claim disputed; - Claim claim = _dummyClaim(); - uint256 splitDepth = gameProxy.splitDepth(); - uint64 halfGameDuration = gameProxy.maxClockDuration().raw(); - uint64 clockExtension = gameProxy.clockExtension().raw(); - - // Work our way down to the split depth without moving ahead in time, we don't care about - // the exact clock here, just don't want take the clock below the clock extension time that - // we're trying to test here. - for (uint256 i = 0; i < gameProxy.maxGameDepth() - 2; i++) { - bond = _getRequiredBond(i); - (,,,, disputed,,) = gameProxy.claimData(i); - gameProxy.attack{ value: bond }(disputed, i, claim); - - // Change the claim status when we're crossing the split depth. - if (i == splitDepth - 2) { - claim = _changeClaimStatus(claim, VMStatuses.PANIC); - } - } - - // Warp ahead to the very first timestamp where a clock extension should be triggered. - vm.warp(block.timestamp + halfGameDuration - (clockExtension + gameProxy.vm().oracle().challengePeriod()) + 1); - - // Execute a move that should cause a clock extension. - bond = _getRequiredBond(gameProxy.maxGameDepth() - 2); - (,,,, disputed,,) = gameProxy.claimData(gameProxy.maxGameDepth() - 2); - gameProxy.attack{ value: bond }(disputed, gameProxy.maxGameDepth() - 2, claim); - (,,,,,, clock) = gameProxy.claimData(gameProxy.maxGameDepth() - 1); - - // The clock should have been pushed back to the clock extension time. - assertEq( - clock.duration().raw(), halfGameDuration - (clockExtension + gameProxy.vm().oracle().challengePeriod()) - ); - } - - /// @notice Tests that an identical claim cannot be made twice. The duplicate claim attempt - /// should revert with the `ClaimAlreadyExists` error. - function test_move_duplicateClaim_reverts() public { - Claim claim = _dummyClaim(); - - // Make the first move. This should succeed. - uint256 bond = _getRequiredBond(0); - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: bond }(disputed, 0, claim); - - // Attempt to make the same move again. - vm.expectRevert(ClaimAlreadyExists.selector); - gameProxy.attack{ value: bond }(disputed, 0, claim); - } - - /// @notice Static unit test asserting that identical claims at the same position can be made - /// in different subgames. - function test_move_duplicateClaimsDifferentSubgames_succeeds() public { - Claim claimA = _dummyClaim(); - Claim claimB = _dummyClaim(); - - // Make the first moves. This should succeed. - uint256 bond = _getRequiredBond(0); - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: bond }(disputed, 0, claimA); - gameProxy.attack{ value: bond }(disputed, 0, claimB); - - // Perform an attack at the same position with the same claim value in both subgames. - // These both should succeed. - bond = _getRequiredBond(1); - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.attack{ value: bond }(disputed, 1, claimA); - bond = _getRequiredBond(2); - (,,,, disputed,,) = gameProxy.claimData(2); - gameProxy.attack{ value: bond }(disputed, 2, claimA); - } - - /// @notice Static unit test for the correctness of an opening attack. - function test_move_simpleAttack_succeeds() public { - // Warp ahead 5 seconds. - vm.warp(block.timestamp + 5); - - Claim counter = _dummyClaim(); - - // Perform the attack. - uint256 reqBond = _getRequiredBond(0); - vm.expectEmit(true, true, true, false); - emit Move(0, counter, address(this)); - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: reqBond }(disputed, 0, counter); - - // Grab the claim data of the attack. - ( - uint32 parentIndex, - address counteredBy, - address claimant, - uint128 bond, - Claim claim, - Position position, - Clock clock - ) = gameProxy.claimData(1); - - // Assert correctness of the attack claim's data. - assertEq(parentIndex, 0); - assertEq(counteredBy, address(0)); - assertEq(claimant, address(this)); - assertEq(bond, reqBond); - assertEq(claim.raw(), counter.raw()); - assertEq(position.raw(), Position.wrap(1).move(true).raw()); - assertEq(clock.raw(), LibClock.wrap(Duration.wrap(5), Timestamp.wrap(uint64(block.timestamp))).raw()); - - // Grab the claim data of the parent. - (parentIndex, counteredBy, claimant, bond, claim, position, clock) = gameProxy.claimData(0); - - // Assert correctness of the parent claim's data. - assertEq(parentIndex, type(uint32).max); - assertEq(counteredBy, address(0)); - assertEq(claimant, address(this)); - assertEq(bond, initBond); - assertEq(claim.raw(), ROOT_CLAIM.raw()); - assertEq(position.raw(), 1); - assertEq(clock.raw(), LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp - 5))).raw()); - } - - /// @notice Tests that making a claim at the execution trace bisection root level with an - /// invalid status byte reverts with the `UnexpectedRootClaim` error. - function test_move_incorrectStatusExecRoot_reverts() public { - Claim disputed; - for (uint256 i; i < 4; i++) { - (,,,, disputed,,) = gameProxy.claimData(i); - gameProxy.attack{ value: _getRequiredBond(i) }(disputed, i, _dummyClaim()); - } - - uint256 bond = _getRequiredBond(4); - (,,,, disputed,,) = gameProxy.claimData(4); - vm.expectRevert(abi.encodeWithSelector(UnexpectedRootClaim.selector, bytes32(0))); - gameProxy.attack{ value: bond }(disputed, 4, Claim.wrap(bytes32(0))); - } - - /// @notice Tests that making a claim at the execution trace bisection root level with a valid - /// status byte succeeds. - function test_move_correctStatusExecRoot_succeeds() public { - Claim disputed; - for (uint256 i; i < 4; i++) { - uint256 bond = _getRequiredBond(i); - (,,,, disputed,,) = gameProxy.claimData(i); - gameProxy.attack{ value: bond }(disputed, i, _dummyClaim()); - } - uint256 lastBond = _getRequiredBond(4); - (,,,, disputed,,) = gameProxy.claimData(4); - gameProxy.attack{ value: lastBond }(disputed, 4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); - } - - /// @notice Static unit test asserting that a move reverts when the bonded amount is incorrect. - function test_move_incorrectBondAmount_reverts() public { - (,,,, Claim disputed,,) = gameProxy.claimData(0); - vm.expectRevert(IncorrectBondAmount.selector); - gameProxy.attack{ value: 0 }(disputed, 0, _dummyClaim()); - } - - /// @notice Static unit test asserting that a move reverts when the disputed claim does not - /// match its index. - function test_move_incorrectDisputedIndex_reverts() public { - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: _getRequiredBond(0) }(disputed, 0, _dummyClaim()); - uint256 bond = _getRequiredBond(1); - vm.expectRevert(InvalidDisputedClaimIndex.selector); - gameProxy.attack{ value: bond }(disputed, 1, _dummyClaim()); - } -} - -/// @title FaultDisputeGame_AddLocalData_Test -/// @notice Tests the addLocalData functionality of the `FaultDisputeGame` contract. -contract FaultDisputeGameV2_AddLocalData_Test is FaultDisputeGameV2_TestInit { - /// @notice Tests that adding local data with an out of bounds identifier reverts. - function testFuzz_addLocalData_oob_reverts(uint256 _ident) public { - Claim disputed; - // Get a claim below the split depth so that we can add local data for an execution trace - // subgame. - for (uint256 i; i < 4; i++) { - uint256 bond = _getRequiredBond(i); - (,,,, disputed,,) = gameProxy.claimData(i); - gameProxy.attack{ value: bond }(disputed, i, _dummyClaim()); - } - uint256 lastBond = _getRequiredBond(4); - (,,,, disputed,,) = gameProxy.claimData(4); - gameProxy.attack{ value: lastBond }(disputed, 4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); - - // [1, 5] are valid local data identifiers. - if (_ident <= 5) _ident = 0; - - vm.expectRevert(InvalidLocalIdent.selector); - gameProxy.addLocalData(_ident, 5, 0); - } - - /// @notice Tests that local data is loaded into the preimage oracle correctly in the subgame - /// that is disputing the transition from `GENESIS -> GENESIS + 1` - function test_addLocalDataGenesisTransition_static_succeeds() public { - IPreimageOracle oracle = IPreimageOracle(address(gameProxy.vm().oracle())); - Claim disputed; - - // Get a claim below the split depth so that we can add local data for an execution trace - // subgame. - for (uint256 i; i < 4; i++) { - uint256 bond = _getRequiredBond(i); - (,,,, disputed,,) = gameProxy.claimData(i); - gameProxy.attack{ value: bond }(disputed, i, Claim.wrap(bytes32(i))); - } - uint256 lastBond = _getRequiredBond(4); - (,,,, disputed,,) = gameProxy.claimData(4); - gameProxy.attack{ value: lastBond }(disputed, 4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); - - // Expected start/disputed claims - (Hash root,) = gameProxy.startingOutputRoot(); - bytes32 startingClaim = root.raw(); - bytes32 disputedClaim = bytes32(uint256(3)); - Position disputedPos = LibPosition.wrap(4, 0); - - // Expected local data - bytes32[5] memory data = [ - gameProxy.l1Head().raw(), - startingClaim, - disputedClaim, - bytes32(validL2BlockNumber << 0xC0), - bytes32(gameProxy.l2ChainId() << 0xC0) - ]; - - for (uint256 i = 1; i <= 5; i++) { - uint256 expectedLen = i > 3 ? 8 : 32; - bytes32 key = _getKey(i, keccak256(abi.encode(disputedClaim, disputedPos))); - - gameProxy.addLocalData(i, 5, 0); - (bytes32 dat, uint256 datLen) = oracle.readPreimage(key, 0); - assertEq(dat >> 0xC0, bytes32(expectedLen)); - // Account for the length prefix if i > 3 (the data stored at identifiers i <= 3 are - // 32 bytes long, so the expected length is already correct. If i > 3, the data is only - // 8 bytes long, so the length prefix + the data is 16 bytes total.) - assertEq(datLen, expectedLen + (i > 3 ? 8 : 0)); - - gameProxy.addLocalData(i, 5, 8); - (dat, datLen) = oracle.readPreimage(key, 8); - assertEq(dat, data[i - 1]); - assertEq(datLen, expectedLen); - } - } - - /// @notice Tests that local data is loaded into the preimage oracle correctly. - function test_addLocalDataMiddle_static_succeeds() public { - IPreimageOracle oracle = IPreimageOracle(address(gameProxy.vm().oracle())); - Claim disputed; - - // Get a claim below the split depth so that we can add local data for an execution trace - // subgame. - for (uint256 i; i < 4; i++) { - uint256 bond = _getRequiredBond(i); - (,,,, disputed,,) = gameProxy.claimData(i); - gameProxy.attack{ value: bond }(disputed, i, Claim.wrap(bytes32(i))); - } - uint256 lastBond = _getRequiredBond(4); - (,,,, disputed,,) = gameProxy.claimData(4); - gameProxy.defend{ value: lastBond }(disputed, 4, _changeClaimStatus(ROOT_CLAIM, VMStatuses.VALID)); - - // Expected start/disputed claims - bytes32 startingClaim = bytes32(uint256(3)); - Position startingPos = LibPosition.wrap(4, 0); - bytes32 disputedClaim = bytes32(uint256(2)); - Position disputedPos = LibPosition.wrap(3, 0); - - // Expected local data - bytes32[5] memory data = [ - gameProxy.l1Head().raw(), - startingClaim, - disputedClaim, - bytes32(validL2BlockNumber << 0xC0), - bytes32(gameProxy.l2ChainId() << 0xC0) - ]; - - for (uint256 i = 1; i <= 5; i++) { - uint256 expectedLen = i > 3 ? 8 : 32; - bytes32 key = _getKey(i, keccak256(abi.encode(startingClaim, startingPos, disputedClaim, disputedPos))); - - gameProxy.addLocalData(i, 5, 0); - (bytes32 dat, uint256 datLen) = oracle.readPreimage(key, 0); - assertEq(dat >> 0xC0, bytes32(expectedLen)); - // Account for the length prefix if i > 3 (the data stored at identifiers i <= 3 are - // 32 bytes long, so the expected length is already correct. If i > 3, the data is only - // 8 bytes long, so the length prefix + the data is 16 bytes total.) - assertEq(datLen, expectedLen + (i > 3 ? 8 : 0)); - - gameProxy.addLocalData(i, 5, 8); - (dat, datLen) = oracle.readPreimage(key, 8); - assertEq(dat, data[i - 1]); - assertEq(datLen, expectedLen); - } - } - - /// @notice Tests that the L2 block number claim is favored over the bisected-to block when - /// adding data. - function test_addLocalData_l2BlockNumberExtension_succeeds() public { - // Deploy a new dispute game with a L2 block number claim of 8. This is directly in the - // middle of the leaves in our output bisection test tree, at SPLIT_DEPTH = 2 ** 2 - IFaultDisputeGameV2 game = IFaultDisputeGameV2( - address( - disputeGameFactory.create{ value: initBond }( - GAME_TYPE, Claim.wrap(bytes32(uint256(0xFF))), abi.encode(validL2BlockNumber) - ) - ) - ); - - // Get a claim below the split depth so that we can add local data for an execution trace - // subgame. - { - Claim disputed; - Position parent; - Position pos; - - for (uint256 i; i < 4; i++) { - (,,,,, parent,) = game.claimData(i); - pos = parent.move(true); - uint256 bond = game.getRequiredBond(pos); - - (,,,, disputed,,) = game.claimData(i); - if (i == 0) { - game.attack{ value: bond }(disputed, i, Claim.wrap(bytes32(i))); - } else { - game.defend{ value: bond }(disputed, i, Claim.wrap(bytes32(i))); - } - } - (,,,,, parent,) = game.claimData(4); - pos = parent.move(true); - uint256 lastBond = game.getRequiredBond(pos); - (,,,, disputed,,) = game.claimData(4); - game.defend{ value: lastBond }(disputed, 4, _changeClaimStatus(ROOT_CLAIM, VMStatuses.INVALID)); - } - - // Expected start/disputed claims - bytes32 startingClaim = bytes32(uint256(3)); - Position startingPos = LibPosition.wrap(4, 14); - bytes32 disputedClaim = bytes32(uint256(0xFF)); - Position disputedPos = LibPosition.wrap(0, 0); - - // Expected local data. This should be `l2BlockNumber`, and not the actual bisected-to - // block, as we choose the minimum between the two. - bytes32 expectedNumber = bytes32(validL2BlockNumber << 0xC0); - uint256 expectedLen = 8; - uint256 l2NumberIdent = LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER; - - // Compute the preimage key for the local data - bytes32 localContext = keccak256(abi.encode(startingClaim, startingPos, disputedClaim, disputedPos)); - bytes32 rawKey = keccak256(abi.encode(l2NumberIdent | (1 << 248), address(game), localContext)); - bytes32 key = bytes32((uint256(rawKey) & ~uint256(0xFF << 248)) | (1 << 248)); - - IPreimageOracle oracle = IPreimageOracle(address(game.vm().oracle())); - game.addLocalData(l2NumberIdent, 5, 0); - - (bytes32 dat, uint256 datLen) = oracle.readPreimage(key, 0); - assertEq(dat >> 0xC0, bytes32(expectedLen)); - assertEq(datLen, expectedLen + 8); - - game.addLocalData(l2NumberIdent, 5, 8); - (dat, datLen) = oracle.readPreimage(key, 8); - assertEq(dat, expectedNumber); - assertEq(datLen, expectedLen); - } -} - -/// @title FaultDisputeGame_ChallengeRootL2Block_Test -/// @notice Tests the challengeRootL2Block functionality of the `FaultDisputeGame` contract. -contract FaultDisputeGameV2_ChallengeRootL2Block_Test is FaultDisputeGameV2_TestInit { - /// @notice Tests that challenging the root claim's L2 block number by providing the real - /// preimage of the output root succeeds. - function testFuzz_challengeRootL2Block_succeeds( - bytes32 _storageRoot, - bytes32 _withdrawalRoot, - uint256 _l2BlockNumber - ) - public - { - _l2BlockNumber = bound(_l2BlockNumber, validL2BlockNumber, type(uint256).max - 1); - - (Types.OutputRootProof memory outputRootProof, bytes32 outputRoot, bytes memory headerRLP) = - _generateOutputRootProof(_storageRoot, _withdrawalRoot, abi.encodePacked(_l2BlockNumber)); - - // Create the dispute game with the output root at the wrong L2 block number. - uint256 wrongL2BlockNumber = bound(vm.randomUint(), _l2BlockNumber + 1, type(uint256).max); - IDisputeGame game = disputeGameFactory.create{ value: initBond }( - GAME_TYPE, Claim.wrap(outputRoot), abi.encode(wrongL2BlockNumber) - ); - - // Challenge the L2 block number. - IFaultDisputeGameV2 fdg = IFaultDisputeGameV2(address(game)); - fdg.challengeRootL2Block(outputRootProof, headerRLP); - - // Ensure that a duplicate challenge reverts. - vm.expectRevert(L2BlockNumberChallenged.selector); - fdg.challengeRootL2Block(outputRootProof, headerRLP); - - // Warp past the clocks, resolve the game. - vm.warp(block.timestamp + 3 days + 12 hours + 1); - fdg.resolveClaim(0, 0); - fdg.resolve(); - - // Ensure the challenge was successful. - assertEq(uint8(fdg.status()), uint8(GameStatus.CHALLENGER_WINS)); - assertTrue(fdg.l2BlockNumberChallenged()); - } - - /// @notice Tests that challenging the root claim's L2 block number by providing the real - /// preimage of the output root succeeds. Also, this claim should always receive the - /// bond when there is another counter that is as far left as possible. - function testFuzz_challengeRootL2Block_receivesBond_succeeds( - bytes32 _storageRoot, - bytes32 _withdrawalRoot, - uint256 _l2BlockNumber - ) - public - { - vm.deal(address(0xb0b), 1 ether); - _l2BlockNumber = bound(_l2BlockNumber, validL2BlockNumber, type(uint256).max - 1); - - (Types.OutputRootProof memory outputRootProof, bytes32 outputRoot, bytes memory headerRLP) = - _generateOutputRootProof(_storageRoot, _withdrawalRoot, abi.encodePacked(_l2BlockNumber)); - - // Create the dispute game with the output root at the wrong L2 block number. - disputeGameFactory.setInitBond(GAME_TYPE, 0.1 ether); - uint256 balanceBefore = address(this).balance; - _l2BlockNumber = bound(vm.randomUint(), _l2BlockNumber + 1, type(uint256).max); - IDisputeGame game = - disputeGameFactory.create{ value: 0.1 ether }(GAME_TYPE, Claim.wrap(outputRoot), abi.encode(_l2BlockNumber)); - IFaultDisputeGameV2 fdg = IFaultDisputeGameV2(address(game)); - - // Attack the root as 0xb0b - uint256 bond = _getRequiredBond(0); - (,,,, Claim disputed,,) = fdg.claimData(0); - vm.prank(address(0xb0b)); - fdg.attack{ value: bond }(disputed, 0, Claim.wrap(0)); - - // Challenge the L2 block number as 0xace. This claim should receive the root claim's bond. - vm.prank(address(0xace)); - fdg.challengeRootL2Block(outputRootProof, headerRLP); - - // Warp past the clocks, resolve the game. - vm.warp(block.timestamp + 3 days + 12 hours + 1); - fdg.resolveClaim(1, 0); - fdg.resolveClaim(0, 0); - fdg.resolve(); - - // Ensure the challenge was successful. - assertEq(uint8(fdg.status()), uint8(GameStatus.CHALLENGER_WINS)); - - // Wait for finalization delay. - vm.warp(block.timestamp + 3.5 days + 1 seconds); - - // Close the game. - fdg.closeGame(); - - // Claim credit once to trigger unlock period. - fdg.claimCredit(address(this)); - fdg.claimCredit(address(0xb0b)); - fdg.claimCredit(address(0xace)); - - // Wait for the withdrawal delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); - - // Claim credit - vm.expectRevert(NoCreditToClaim.selector); - fdg.claimCredit(address(this)); - fdg.claimCredit(address(0xb0b)); - fdg.claimCredit(address(0xace)); - - // Ensure that the party who challenged the L2 block number with the special move received - // the bond. - // - Root claim loses their bond - // - 0xace receives the root claim's bond - // - 0xb0b receives their bond back - assertEq(address(this).balance, balanceBefore - 0.1 ether); - assertEq(address(0xb0b).balance, 1 ether); - assertEq(address(0xace).balance, 0.1 ether); - } - - /// @notice Tests that challenging the root claim's L2 block number by providing the real - /// preimage of the output root never succeeds. - function testFuzz_challengeRootL2Block_rightBlockNumber_reverts( - bytes32 _storageRoot, - bytes32 _withdrawalRoot, - uint256 _l2BlockNumber - ) - public - { - _l2BlockNumber = bound(_l2BlockNumber, validL2BlockNumber, type(uint256).max); - - (Types.OutputRootProof memory outputRootProof, bytes32 outputRoot, bytes memory headerRLP) = - _generateOutputRootProof(_storageRoot, _withdrawalRoot, abi.encodePacked(_l2BlockNumber)); - - // Create the dispute game with the output root at the wrong L2 block number. - IDisputeGame game = - disputeGameFactory.create{ value: initBond }(GAME_TYPE, Claim.wrap(outputRoot), abi.encode(_l2BlockNumber)); - - // Challenge the L2 block number. - IFaultDisputeGameV2 fdg = IFaultDisputeGameV2(address(game)); - vm.expectRevert(BlockNumberMatches.selector); - fdg.challengeRootL2Block(outputRootProof, headerRLP); - - // Warp past the clocks, resolve the game. - vm.warp(block.timestamp + 3 days + 12 hours + 1); - fdg.resolveClaim(0, 0); - fdg.resolve(); - - // Ensure the challenge was successful. - assertEq(uint8(fdg.status()), uint8(GameStatus.DEFENDER_WINS)); - } - - /// @notice Tests that challenging the root claim's L2 block number with a bad output root - /// proof reverts. - function test_challengeRootL2Block_badProof_reverts() public { - Types.OutputRootProof memory outputRootProof = - Types.OutputRootProof({ version: 0, stateRoot: 0, messagePasserStorageRoot: 0, latestBlockhash: 0 }); - - vm.expectRevert(InvalidOutputRootProof.selector); - gameProxy.challengeRootL2Block(outputRootProof, hex""); - } - - /// @notice Tests that challenging the root claim's L2 block number with a bad output root - /// proof reverts. - function test_challengeRootL2Block_badHeaderRLP_reverts() public { - Types.OutputRootProof memory outputRootProof = - Types.OutputRootProof({ version: 0, stateRoot: 0, messagePasserStorageRoot: 0, latestBlockhash: 0 }); - bytes32 outputRoot = Hashing.hashOutputRootProof(outputRootProof); - - // Create the dispute game with the output root at the wrong L2 block number. - IDisputeGame game = disputeGameFactory.create{ value: initBond }( - GAME_TYPE, Claim.wrap(outputRoot), abi.encode(validL2BlockNumber) - ); - IFaultDisputeGameV2 fdg = IFaultDisputeGameV2(address(game)); - - vm.expectRevert(InvalidHeaderRLP.selector); - fdg.challengeRootL2Block(outputRootProof, hex""); - } - - /// @notice Tests that challenging the root claim's L2 block number with a bad output root - /// proof reverts. - function test_challengeRootL2Block_badHeaderRLPBlockNumberLength_reverts() public { - (Types.OutputRootProof memory outputRootProof, bytes32 outputRoot,) = - _generateOutputRootProof(0, 0, new bytes(64)); - - // Create the dispute game with the output root at the wrong L2 block number. - IDisputeGame game = disputeGameFactory.create{ value: initBond }( - GAME_TYPE, Claim.wrap(outputRoot), abi.encode(validL2BlockNumber) - ); - IFaultDisputeGameV2 fdg = IFaultDisputeGameV2(address(game)); - - vm.expectRevert(InvalidHeaderRLP.selector); - fdg.challengeRootL2Block(outputRootProof, hex""); - } -} - -/// @title FaultDisputeGame_Resolve_Test -/// @notice Tests the resolve functionality of the `FaultDisputeGame` contract. -contract FaultDisputeGameV2_Resolve_Test is FaultDisputeGameV2_TestInit { - /// @notice Static unit test for the correctness an uncontested root resolution. - function test_resolve_rootUncontested_succeeds() public { - vm.warp(block.timestamp + 3 days + 12 hours); - gameProxy.resolveClaim(0, 0); - assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); - } - - /// @notice Static unit test for the correctness an uncontested root resolution. - function test_resolve_rootUncontestedClockNotExpired_succeeds() public { - vm.warp(block.timestamp + 3 days + 12 hours - 1 seconds); - vm.expectRevert(ClockNotExpired.selector); - gameProxy.resolveClaim(0, 0); - } - - /// @notice Static unit test for the correctness of a multi-part resolution of a single claim. - function test_resolve_multiPart_succeeds() public { - vm.deal(address(this), 10_000 ether); - - uint256 bond = _getRequiredBond(0); - for (uint256 i = 0; i < 2048; i++) { - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: bond }(disputed, 0, Claim.wrap(bytes32(i))); - } - - // Warp past the clock period. - vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds); - - // Resolve all children of the root subgame. Every single one of these will be uncontested. - for (uint256 i = 1; i <= 2048; i++) { - gameProxy.resolveClaim(i, 0); - } - - // Resolve the first half of the root claim subgame. - gameProxy.resolveClaim(0, 1024); - - // Fetch the resolution checkpoint for the root subgame and assert correctness. - (bool initCheckpoint, uint32 subgameIndex, Position leftmostPosition, address counteredBy) = - gameProxy.resolutionCheckpoints(0); - assertTrue(initCheckpoint); - assertEq(subgameIndex, 1024); - assertEq(leftmostPosition.raw(), Position.wrap(1).move(true).raw()); - assertEq(counteredBy, address(this)); - - // The root subgame should not be resolved. - assertFalse(gameProxy.resolvedSubgames(0)); - vm.expectRevert(OutOfOrderResolution.selector); - gameProxy.resolve(); - - // Resolve the second half of the root claim subgame. - uint256 numToResolve = gameProxy.getNumToResolve(0); - assertEq(numToResolve, 1024); - gameProxy.resolveClaim(0, numToResolve); - - // Fetch the resolution checkpoint for the root subgame and assert correctness. - (initCheckpoint, subgameIndex, leftmostPosition, counteredBy) = gameProxy.resolutionCheckpoints(0); - assertTrue(initCheckpoint); - assertEq(subgameIndex, 2048); - assertEq(leftmostPosition.raw(), Position.wrap(1).move(true).raw()); - assertEq(counteredBy, address(this)); - - // The root subgame should now be resolved - assertTrue(gameProxy.resolvedSubgames(0)); - assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS)); - } - - /// @notice Static unit test asserting that resolve reverts when the absolute root - /// subgame has not been resolved. - function test_resolve_rootUncontestedButUnresolved_reverts() public { - vm.warp(block.timestamp + 3 days + 12 hours); - vm.expectRevert(OutOfOrderResolution.selector); - gameProxy.resolve(); - } - - /// @notice Static unit test asserting that resolve reverts when the game state is - /// not in progress. - function test_resolve_notInProgress_reverts() public { - uint256 chalWins = uint256(GameStatus.CHALLENGER_WINS); - - // Replace the game status in storage. It exists in slot 0 at offset 16. - uint256 slot = uint256(vm.load(address(gameProxy), bytes32(0))); - uint256 offset = 16 << 3; - uint256 mask = 0xFF << offset; - // Replace the byte in the slot value with the challenger wins status. - slot = (slot & ~mask) | (chalWins << offset); - - vm.store(address(gameProxy), bytes32(uint256(0)), bytes32(slot)); - vm.expectRevert(GameNotInProgress.selector); - gameProxy.resolveClaim(0, 0); - } - - /// @notice Static unit test for the correctness of resolving a single attack game state. - function test_resolve_rootContested_succeeds() public { - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: _getRequiredBond(0) }(disputed, 0, _dummyClaim()); - - vm.warp(block.timestamp + 3 days + 12 hours); - - gameProxy.resolveClaim(1, 0); - gameProxy.resolveClaim(0, 0); - assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS)); - } - - /// @notice Static unit test for the correctness of resolving a game with a contested challenge - /// claim. - function test_resolve_challengeContested_succeeds() public { - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: _getRequiredBond(0) }(disputed, 0, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.defend{ value: _getRequiredBond(1) }(disputed, 1, _dummyClaim()); - - vm.warp(block.timestamp + 3 days + 12 hours); - - gameProxy.resolveClaim(2, 0); - gameProxy.resolveClaim(1, 0); - gameProxy.resolveClaim(0, 0); - assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); - } - - /// @notice Static unit test for the correctness of resolving a game with multiplayer moves. - function test_resolve_teamDeathmatch_succeeds() public { - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: _getRequiredBond(0) }(disputed, 0, _dummyClaim()); - gameProxy.attack{ value: _getRequiredBond(0) }(disputed, 0, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.defend{ value: _getRequiredBond(1) }(disputed, 1, _dummyClaim()); - gameProxy.defend{ value: _getRequiredBond(1) }(disputed, 1, _dummyClaim()); - - vm.warp(block.timestamp + 3 days + 12 hours); - - gameProxy.resolveClaim(4, 0); - gameProxy.resolveClaim(3, 0); - gameProxy.resolveClaim(2, 0); - gameProxy.resolveClaim(1, 0); - gameProxy.resolveClaim(0, 0); - assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS)); - } - - /// @notice Static unit test for the correctness of resolving a game that reaches max game - /// depth. - function test_resolve_stepReached_succeeds() public { - Claim claim = _dummyClaim(); - for (uint256 i; i < gameProxy.splitDepth(); i++) { - (,,,, Claim disputed,,) = gameProxy.claimData(i); - gameProxy.attack{ value: _getRequiredBond(i) }(disputed, i, claim); - } - - claim = _changeClaimStatus(claim, VMStatuses.PANIC); - for (uint256 i = gameProxy.claimDataLen() - 1; i < gameProxy.maxGameDepth(); i++) { - (,,,, Claim disputed,,) = gameProxy.claimData(i); - gameProxy.attack{ value: _getRequiredBond(i) }(disputed, i, claim); - } - - vm.warp(block.timestamp + 3 days + 12 hours); - - for (uint256 i = 9; i > 0; i--) { - gameProxy.resolveClaim(i - 1, 0); - } - assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); - } - - /// @notice Static unit test asserting that resolve reverts when attempting to resolve a - /// subgame multiple times - function test_resolve_claimAlreadyResolved_reverts() public { - Claim claim = _dummyClaim(); - uint256 firstBond = _getRequiredBond(0); - vm.deal(address(this), firstBond); - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: firstBond }(disputed, 0, claim); - uint256 secondBond = _getRequiredBond(1); - vm.deal(address(this), secondBond); - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.attack{ value: secondBond }(disputed, 1, claim); - - vm.warp(block.timestamp + 3 days + 12 hours); - - gameProxy.resolveClaim(2, 0); - gameProxy.resolveClaim(1, 0); - - vm.expectRevert(ClaimAlreadyResolved.selector); - gameProxy.resolveClaim(1, 0); - } - - /// @notice Static unit test asserting that resolve reverts when attempting to resolve a - /// subgame at max depth - function test_resolve_claimAtMaxDepthAlreadyResolved_reverts() public { - Claim claim = _dummyClaim(); - for (uint256 i; i < gameProxy.splitDepth(); i++) { - (,,,, Claim disputed,,) = gameProxy.claimData(i); - gameProxy.attack{ value: _getRequiredBond(i) }(disputed, i, claim); - } - - vm.deal(address(this), 10000 ether); - claim = _changeClaimStatus(claim, VMStatuses.PANIC); - for (uint256 i = gameProxy.claimDataLen() - 1; i < gameProxy.maxGameDepth(); i++) { - (,,,, Claim disputed,,) = gameProxy.claimData(i); - gameProxy.attack{ value: _getRequiredBond(i) }(disputed, i, claim); - } - - vm.warp(block.timestamp + 3 days + 12 hours); - - gameProxy.resolveClaim(8, 0); - - vm.expectRevert(ClaimAlreadyResolved.selector); - gameProxy.resolveClaim(8, 0); - } - - /// @notice Static unit test asserting that resolve reverts when attempting to resolve - /// subgames out of order - function test_resolve_outOfOrderResolution_reverts() public { - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: _getRequiredBond(0) }(disputed, 0, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.attack{ value: _getRequiredBond(1) }(disputed, 1, _dummyClaim()); - - vm.warp(block.timestamp + 3 days + 12 hours); - - vm.expectRevert(OutOfOrderResolution.selector); - gameProxy.resolveClaim(0, 0); - } - - /// @notice Static unit test asserting that resolve pays out bonds on step, output bisection, - /// and execution trace moves. - function test_resolve_bondPayouts_succeeds() public { - // Give the test contract some ether - uint256 bal = 1000 ether; - vm.deal(address(this), bal); - - // Make claims all the way down the tree. - uint256 bond = _getRequiredBond(0); - uint256 totalBonded = bond; - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: bond }(disputed, 0, _dummyClaim()); - bond = _getRequiredBond(1); - totalBonded += bond; - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.attack{ value: bond }(disputed, 1, _dummyClaim()); - bond = _getRequiredBond(2); - totalBonded += bond; - (,,,, disputed,,) = gameProxy.claimData(2); - gameProxy.attack{ value: bond }(disputed, 2, _dummyClaim()); - bond = _getRequiredBond(3); - totalBonded += bond; - (,,,, disputed,,) = gameProxy.claimData(3); - gameProxy.attack{ value: bond }(disputed, 3, _dummyClaim()); - bond = _getRequiredBond(4); - totalBonded += bond; - (,,,, disputed,,) = gameProxy.claimData(4); - gameProxy.attack{ value: bond }(disputed, 4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); - bond = _getRequiredBond(5); - totalBonded += bond; - (,,,, disputed,,) = gameProxy.claimData(5); - gameProxy.attack{ value: bond }(disputed, 5, _dummyClaim()); - bond = _getRequiredBond(6); - totalBonded += bond; - (,,,, disputed,,) = gameProxy.claimData(6); - gameProxy.attack{ value: bond }(disputed, 6, _dummyClaim()); - bond = _getRequiredBond(7); - totalBonded += bond; - (,,,, disputed,,) = gameProxy.claimData(7); - gameProxy.attack{ value: bond }(disputed, 7, _dummyClaim()); - gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); - gameProxy.step(8, true, absolutePrestateData, hex""); - - // Ensure that the step successfully countered the leaf claim. - (, address counteredBy,,,,,) = gameProxy.claimData(8); - assertEq(counteredBy, address(this)); - - // Ensure we bonded the correct amounts - assertEq(address(this).balance, bal - totalBonded); - assertEq(address(gameProxy).balance, 0); - assertEq(delayedWeth.balanceOf(address(gameProxy)), initBond + totalBonded); - - // Resolve all claims - vm.warp(block.timestamp + 3 days + 12 hours); - for (uint256 i = gameProxy.claimDataLen(); i > 0; i--) { - (bool success,) = address(gameProxy).call(abi.encodeCall(gameProxy.resolveClaim, (i - 1, 0))); - assertTrue(success); - } - gameProxy.resolve(); - - // Wait for finalization delay - vm.warp(block.timestamp + 3.5 days + 1 seconds); - - // Close the game. - gameProxy.closeGame(); - - // Claim credit once to trigger unlock period. - gameProxy.claimCredit(address(this)); - - // Wait for the withdrawal delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); - - // Claim credit again to get the bond back. - gameProxy.claimCredit(address(this)); - - // Ensure that bonds were paid out correctly. - assertEq(address(this).balance, bal + initBond); - assertEq(address(gameProxy).balance, 0); - assertEq(delayedWeth.balanceOf(address(gameProxy)), 0); - - // Ensure that the init bond for the game is 0, in case we change it in the test suite in - // the future. - assertEq(disputeGameFactory.initBonds(GAME_TYPE), initBond); - } - - /// @notice Static unit test asserting that resolve pays out bonds on step, output bisection, - /// and execution trace moves with 2 actors and a dishonest root claim. - function test_resolve_bondPayoutsSeveralActors_succeeds() public { - // Give the test contract and bob some ether - // We use the "1000 ether" literal for `bal`, the initial balance, to avoid stack too deep - //uint256 bal = 1000 ether; - address bob = address(0xb0b); - vm.deal(address(this), 1000 ether); - vm.deal(bob, 1000 ether); - - // Make claims all the way down the tree, trading off between bob and the test contract. - uint256 firstBond = _getRequiredBond(0); - uint256 thisBonded = firstBond; - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: firstBond }(disputed, 0, _dummyClaim()); - - uint256 secondBond = _getRequiredBond(1); - uint256 bobBonded = secondBond; - (,,,, disputed,,) = gameProxy.claimData(1); - vm.prank(bob); - gameProxy.attack{ value: secondBond }(disputed, 1, _dummyClaim()); - - uint256 thirdBond = _getRequiredBond(2); - thisBonded += thirdBond; - (,,,, disputed,,) = gameProxy.claimData(2); - gameProxy.attack{ value: thirdBond }(disputed, 2, _dummyClaim()); - - uint256 fourthBond = _getRequiredBond(3); - bobBonded += fourthBond; - (,,,, disputed,,) = gameProxy.claimData(3); - vm.prank(bob); - gameProxy.attack{ value: fourthBond }(disputed, 3, _dummyClaim()); - - uint256 fifthBond = _getRequiredBond(4); - thisBonded += fifthBond; - (,,,, disputed,,) = gameProxy.claimData(4); - gameProxy.attack{ value: fifthBond }(disputed, 4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); - - uint256 sixthBond = _getRequiredBond(5); - bobBonded += sixthBond; - (,,,, disputed,,) = gameProxy.claimData(5); - vm.prank(bob); - gameProxy.attack{ value: sixthBond }(disputed, 5, _dummyClaim()); - - uint256 seventhBond = _getRequiredBond(6); - thisBonded += seventhBond; - (,,,, disputed,,) = gameProxy.claimData(6); - gameProxy.attack{ value: seventhBond }(disputed, 6, _dummyClaim()); - - uint256 eighthBond = _getRequiredBond(7); - bobBonded += eighthBond; - (,,,, disputed,,) = gameProxy.claimData(7); - vm.prank(bob); - gameProxy.attack{ value: eighthBond }(disputed, 7, _dummyClaim()); - - gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); - gameProxy.step(8, true, absolutePrestateData, hex""); - - // Ensure that the step successfully countered the leaf claim. - (, address counteredBy,,,,,) = gameProxy.claimData(8); - assertEq(counteredBy, address(this)); - - // Ensure we bonded the correct amounts - assertEq(address(this).balance, 1000 ether - thisBonded); - assertEq(bob.balance, 1000 ether - bobBonded); - assertEq(address(gameProxy).balance, 0); - assertEq(delayedWeth.balanceOf(address(gameProxy)), initBond + thisBonded + bobBonded); - - // Resolve all claims - vm.warp(block.timestamp + 3 days + 12 hours); - for (uint256 i = gameProxy.claimDataLen(); i > 0; i--) { - (bool success,) = address(gameProxy).call(abi.encodeCall(gameProxy.resolveClaim, (i - 1, 0))); - assertTrue(success); - } - - // Resolve the game. - gameProxy.resolve(); - - // Wait for finalization delay - vm.warp(block.timestamp + 3.5 days + 1 seconds); - - // Close the game. - gameProxy.closeGame(); - - // Claim credit once to trigger unlock period. - gameProxy.claimCredit(address(this)); - gameProxy.claimCredit(bob); - - // Wait for the withdrawal delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); - - // Claim credit again to get the bond back. - gameProxy.claimCredit(address(this)); - - // Bob's claim should revert since it's value is 0 - vm.expectRevert(NoCreditToClaim.selector); - gameProxy.claimCredit(bob); - - // Ensure that bonds were paid out correctly. - assertEq(address(this).balance, 1000 ether + initBond + bobBonded); - assertEq(bob.balance, 1000 ether - bobBonded); - assertEq(address(gameProxy).balance, 0); - assertEq(delayedWeth.balanceOf(address(gameProxy)), 0); - - // Ensure that the init bond for the game is 0, in case we change it in the test suite in - // the future. - assertEq(disputeGameFactory.initBonds(GAME_TYPE), initBond); - } - - /// @notice Static unit test asserting that resolve pays out bonds on moves to the leftmost - /// actor in subgames containing successful counters. - function test_resolve_leftmostBondPayout_succeeds() public { - uint256 bal = 1000 ether; - address alice = address(0xa11ce); - address bob = address(0xb0b); - address charlie = address(0xc0c); - vm.deal(address(this), bal); - vm.deal(alice, bal); - vm.deal(bob, bal); - vm.deal(charlie, bal); - - // Make claims with bob, charlie and the test contract on defense, and alice as the - // challenger charlie is successfully countered by alice alice is successfully countered by - // both bob and the test contract - uint256 firstBond = _getRequiredBond(0); - (,,,, Claim disputed,,) = gameProxy.claimData(0); - vm.prank(alice); - gameProxy.attack{ value: firstBond }(disputed, 0, _dummyClaim()); - - uint256 secondBond = _getRequiredBond(1); - (,,,, disputed,,) = gameProxy.claimData(1); - vm.prank(bob); - gameProxy.defend{ value: secondBond }(disputed, 1, _dummyClaim()); - vm.prank(charlie); - gameProxy.attack{ value: secondBond }(disputed, 1, _dummyClaim()); - gameProxy.attack{ value: secondBond }(disputed, 1, _dummyClaim()); - - uint256 thirdBond = _getRequiredBond(3); - (,,,, disputed,,) = gameProxy.claimData(3); - vm.prank(alice); - gameProxy.attack{ value: thirdBond }(disputed, 3, _dummyClaim()); - - // Resolve all claims - vm.warp(block.timestamp + 3 days + 12 hours); - for (uint256 i = gameProxy.claimDataLen(); i > 0; i--) { - (bool success,) = address(gameProxy).call(abi.encodeCall(gameProxy.resolveClaim, (i - 1, 0))); - assertTrue(success); - } - gameProxy.resolve(); - - // Wait for finalization delay - vm.warp(block.timestamp + 3.5 days + 1 seconds); - - // Close the game. - gameProxy.closeGame(); - - // Claim credit once to trigger unlock period. - gameProxy.claimCredit(address(this)); - gameProxy.claimCredit(alice); - gameProxy.claimCredit(bob); - gameProxy.claimCredit(charlie); - - // Wait for the withdrawal delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); - - // All of these claims should work. - gameProxy.claimCredit(address(this)); - gameProxy.claimCredit(alice); - gameProxy.claimCredit(bob); - - // Charlie's claim should revert since it's value is 0 - vm.expectRevert(NoCreditToClaim.selector); - gameProxy.claimCredit(charlie); - - // Ensure that bonds were paid out correctly. - uint256 aliceLosses = firstBond; - uint256 charlieLosses = secondBond; - assertEq(address(this).balance, bal + aliceLosses + initBond, "incorrect this balance"); - assertEq(alice.balance, bal - aliceLosses + charlieLosses, "incorrect alice balance"); - assertEq(bob.balance, bal, "incorrect bob balance"); - assertEq(charlie.balance, bal - charlieLosses, "incorrect charlie balance"); - assertEq(address(gameProxy).balance, 0); - - // Ensure that the init bond for the game is 0, in case we change it in the test suite in - // the future. - assertEq(disputeGameFactory.initBonds(GAME_TYPE), initBond); - } - - /// @notice Static unit test asserting that the anchor state updates when the game resolves in - /// favor of the defender and the anchor state is older than the game state. - function test_resolve_validNewerStateUpdatesAnchor_succeeds() public { - // Confirm that the anchor state is older than the game state. - (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - assert(l2BlockNumber < gameProxy.l2BlockNumber()); - - // Resolve the game. - vm.warp(block.timestamp + 3 days + 12 hours); - gameProxy.resolveClaim(0, 0); - assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); - - // Wait for finalization delay. - vm.warp(block.timestamp + 3.5 days + 1 seconds); - - // Close the game. - gameProxy.closeGame(); - - // Confirm that the anchor state is now the same as the game state. - (root, l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - assertEq(l2BlockNumber, gameProxy.l2BlockNumber()); - assertEq(root.raw(), gameProxy.rootClaim().raw()); - } - - /// @notice Static unit test asserting that the anchor state does not change when the game - /// resolves in favor of the defender but the game state is not newer than the anchor - /// state. - function test_resolve_validOlderStateSameAnchor_succeeds() public { - // Mock the game block to be older than the game state. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(0)); - - // Confirm that the anchor state is newer than the game state. - (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - assert(l2BlockNumber >= gameProxy.l2SequenceNumber()); - - // Resolve the game. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(0)); - vm.warp(block.timestamp + 3 days + 12 hours); - gameProxy.resolveClaim(0, 0); - assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); - - // Wait for finalization delay. - vm.warp(block.timestamp + 3.5 days + 1 seconds); - - // Close the game. - gameProxy.closeGame(); - - // Confirm that the anchor state is the same as the initial anchor state. - (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - assertEq(updatedL2BlockNumber, l2BlockNumber); - assertEq(updatedRoot.raw(), root.raw()); - } - - /// @notice Static unit test asserting that the anchor state does not change when the game - /// resolves in favor of the challenger, even if the game state is newer than the - /// anchor state. - function test_resolve_invalidStateSameAnchor_succeeds() public { - // Confirm that the anchor state is older than the game state. - (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - assert(l2BlockNumber < gameProxy.l2BlockNumber()); - - // Challenge the claim and resolve it. - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: _getRequiredBond(0) }(disputed, 0, _dummyClaim()); - vm.warp(block.timestamp + 3 days + 12 hours); - gameProxy.resolveClaim(1, 0); - gameProxy.resolveClaim(0, 0); - assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS)); - - // Wait for finalization delay. - vm.warp(block.timestamp + 3.5 days + 1 seconds); - - // Close the game. - gameProxy.closeGame(); - - // Confirm that the anchor state is the same as the initial anchor state. - (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - assertEq(updatedL2BlockNumber, l2BlockNumber); - assertEq(updatedRoot.raw(), root.raw()); - } -} - -/// @title FaultDisputeGame_GameType_Test -/// @notice Tests the `gameType` function of the `FaultDisputeGame` contract. -contract FaultDisputeGameV2_GameType_Test is FaultDisputeGameV2_TestInit { - /// @notice Tests that the game's type is set correctly. - function test_gameType_succeeds() public view { - assertEq(gameProxy.gameType().raw(), GAME_TYPE.raw()); - } -} - -/// @title FaultDisputeGame_RootClaim_Test -/// @notice Tests the `rootClaim` function of the `FaultDisputeGame` contract. -contract FaultDisputeGameV2_RootClaim_Test is FaultDisputeGameV2_TestInit { - /// @notice Tests that the game's root claim is set correctly. - function test_rootClaim_succeeds() public view { - assertEq(gameProxy.rootClaim().raw(), ROOT_CLAIM.raw()); - } -} - -/// @title FaultDisputeGame_ExtraData_Test -/// @notice Tests the `extraData` function of the `FaultDisputeGame` contract. -contract FaultDisputeGameV2_ExtraData_Test is FaultDisputeGameV2_TestInit { - /// @notice Tests that the game's extra data is set correctly. - function test_extraData_succeeds() public view { - assertEq(gameProxy.extraData(), extraData); - } -} - -/// @title FaultDisputeGame_GameData_Test -/// @notice Tests the `gameData` function of the `FaultDisputeGame` contract. -contract FaultDisputeGameV2_GameData_Test is FaultDisputeGameV2_TestInit { - /// @notice Tests that the game's data is set correctly. - function test_gameData_succeeds() public view { - (GameType gameType, Claim rootClaim, bytes memory _extraData) = gameProxy.gameData(); - - assertEq(gameType.raw(), GAME_TYPE.raw()); - assertEq(rootClaim.raw(), ROOT_CLAIM.raw()); - assertEq(_extraData, extraData); - } -} - -/// @title FaultDisputeGame_GetRequiredBond_Test -/// @notice Tests the `getRequiredBond` function of the `FaultDisputeGame` contract. -contract FaultDisputeGameV2_GetRequiredBond_Test is FaultDisputeGameV2_TestInit { - /// @notice Tests that the bond during the bisection game depths is correct. - function test_getRequiredBond_succeeds() public view { - for (uint8 i = 0; i < uint8(gameProxy.splitDepth()); i++) { - Position pos = LibPosition.wrap(i, 0); - uint256 bond = gameProxy.getRequiredBond(pos); - - // Reasonable approximation for a max depth of 8. - uint256 expected = 0.08 ether; - for (uint64 j = 0; j < i; j++) { - expected = expected * 22876; - expected = expected / 10000; - } - - assertApproxEqAbs(bond, expected, 0.01 ether); - } - } - - /// @notice Tests that the bond at a depth greater than the maximum game depth reverts. - function test_getRequiredBond_outOfBounds_reverts() public { - Position pos = LibPosition.wrap(uint8(gameProxy.maxGameDepth() + 1), 0); - vm.expectRevert(GameDepthExceeded.selector); - gameProxy.getRequiredBond(pos); - } -} - -/// @title FaultDisputeGame_ClaimCredit_Test -/// @notice Tests the claimCredit functionality of the `FaultDisputeGame` contract. -contract FaultDisputeGameV2_ClaimCredit_Test is FaultDisputeGameV2_TestInit { - function test_claimCredit_refundMode_succeeds() public { - // Set up actors. - address alice = address(0xa11ce); - address bob = address(0xb0b); - - // Give the game proxy 1 extra ether, unregistered. - vm.deal(address(gameProxy), 1 ether); - - // Perform a bonded move. - Claim claim = _dummyClaim(); - - // Bond the first claim. - uint256 firstBond = _getRequiredBond(0); - vm.deal(alice, firstBond); - (,,,, Claim disputed,,) = gameProxy.claimData(0); - vm.prank(alice); - gameProxy.attack{ value: firstBond }(disputed, 0, claim); - - // Bond the second claim. - uint256 secondBond = _getRequiredBond(1); - vm.deal(bob, secondBond); - (,,,, disputed,,) = gameProxy.claimData(1); - vm.prank(bob); - gameProxy.attack{ value: secondBond }(disputed, 1, claim); - - // Warp past the finalization period - vm.warp(block.timestamp + 3 days + 12 hours); - - // Resolve the game. - // Second claim wins, so bob should get alice's credit. - gameProxy.resolveClaim(2, 0); - gameProxy.resolveClaim(1, 0); - gameProxy.resolveClaim(0, 0); - gameProxy.resolve(); - - // Wait for finalization delay. - vm.warp(block.timestamp + 3.5 days + 1 seconds); - - // Mock that the game proxy is not proper, trigger refund mode. - vm.mockCall( - address(anchorStateRegistry), - abi.encodeCall(anchorStateRegistry.isGameProper, (gameProxy)), - abi.encode(false) - ); - - // Close the game. - gameProxy.closeGame(); - - // Assert bond distribution mode is refund mode. - assertTrue(gameProxy.bondDistributionMode() == BondDistributionMode.REFUND); - - // Claim credit once to trigger unlock period. - gameProxy.claimCredit(alice); - gameProxy.claimCredit(bob); - - // Wait for the withdrawal delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); - - // Grab balances before claim. - uint256 aliceBalanceBefore = alice.balance; - uint256 bobBalanceBefore = bob.balance; - - // Claim credit again to get the bond back. - gameProxy.claimCredit(alice); - gameProxy.claimCredit(bob); - - // Should have original balance again. - assertEq(alice.balance, aliceBalanceBefore + firstBond); - assertEq(bob.balance, bobBalanceBefore + secondBond); - } - - /// @notice Tests that claimCredit reverts if the game is paused. - function test_claimCredit_gamePaused_reverts() public { - // Pause the system with the Superchain-wide identifier (address(0)). - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(address(0)); - - // Attempting to claim credit should now revert. - vm.expectRevert(GamePaused.selector); - gameProxy.claimCredit(address(0)); - } - - /// @notice Static unit test asserting that credit may not be drained past allowance through - /// reentrancy. - function test_claimCredit_claimAlreadyResolved_reverts() public { - ClaimCreditReenter reenter = new ClaimCreditReenter(gameProxy, vm); - vm.startPrank(address(reenter)); - - // Give the game proxy 1 extra ether, unregistered. - vm.deal(address(gameProxy), 1 ether); - - // Perform a bonded move. - Claim claim = _dummyClaim(); - uint256 firstBond = _getRequiredBond(0); - vm.deal(address(reenter), firstBond); - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: firstBond }(disputed, 0, claim); - uint256 secondBond = _getRequiredBond(1); - vm.deal(address(reenter), secondBond); - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.attack{ value: secondBond }(disputed, 1, claim); - uint256 reenterBond = firstBond + secondBond; - - // Warp past the finalization period - vm.warp(block.timestamp + 3 days + 12 hours); - - // Ensure that we bonded all the test contract's ETH - assertEq(address(reenter).balance, 0); - // Ensure the game proxy has 1 ether in it. - assertEq(address(gameProxy).balance, 1 ether); - // Ensure the game has a balance of reenterBond in the delayedWeth contract. - assertEq(delayedWeth.balanceOf(address(gameProxy)), initBond + reenterBond); - - // Resolve the claim at index 2 first so that index 1 can be resolved. - gameProxy.resolveClaim(2, 0); - - // Resolve the claim at index 1 and claim the reenter contract's credit. - gameProxy.resolveClaim(1, 0); - - // Ensure that the game registered the `reenter` contract's credit. - assertEq(gameProxy.credit(address(reenter)), reenterBond); - - // Resolve the root claim. - gameProxy.resolveClaim(0, 0); - - // Resolve the game. - gameProxy.resolve(); - - // Wait for finalization delay. - vm.warp(block.timestamp + 3.5 days + 1 seconds); - - // Close the game. - gameProxy.closeGame(); - - // Claim credit once to trigger unlock period. - gameProxy.claimCredit(address(reenter)); - - // Wait for the withdrawal delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); - - // Initiate the reentrant credit claim. - reenter.claimCredit(address(reenter)); - - // The reenter contract should have performed 2 calls to `claimCredit`. - // Once all the credit is claimed, all subsequent calls will revert since there is 0 credit - // left to claim. - // The claimant must only have received the amount bonded for the gindex 1 subgame. - // The root claim bond and the unregistered ETH should still exist in the game proxy. - assertEq(reenter.numCalls(), 2); - assertEq(address(reenter).balance, reenterBond); - assertEq(address(gameProxy).balance, 1 ether); - assertEq(delayedWeth.balanceOf(address(gameProxy)), initBond); - - vm.stopPrank(); - } - - /// @notice Tests that claimCredit reverts when recipient can't receive value. - function test_claimCredit_recipientCantReceiveValue_reverts() public { - // Set up actors. - address alice = address(0xa11ce); - address bob = address(0xb0b); - - // Give the game proxy 1 extra ether, unregistered. - vm.deal(address(gameProxy), 1 ether); - - // Perform a bonded move. - Claim claim = _dummyClaim(); - - // Bond the first claim. - uint256 firstBond = _getRequiredBond(0); - vm.deal(alice, firstBond); - (,,,, Claim disputed,,) = gameProxy.claimData(0); - vm.prank(alice); - gameProxy.attack{ value: firstBond }(disputed, 0, claim); - - // Bond the second claim. - uint256 secondBond = _getRequiredBond(1); - vm.deal(bob, secondBond); - (,,,, disputed,,) = gameProxy.claimData(1); - vm.prank(bob); - gameProxy.attack{ value: secondBond }(disputed, 1, claim); - - // Warp past the finalization period - vm.warp(block.timestamp + 3 days + 12 hours); - - // Resolve the game. - // Second claim wins, so bob should get alice's credit. - gameProxy.resolveClaim(2, 0); - gameProxy.resolveClaim(1, 0); - gameProxy.resolveClaim(0, 0); - gameProxy.resolve(); - - // Wait for finalization delay. - vm.warp(block.timestamp + 3.5 days + 1 seconds); - - // Close the game. - gameProxy.closeGame(); - - // Claim credit once to trigger unlock period. - gameProxy.claimCredit(alice); - gameProxy.claimCredit(bob); - - // Wait for the withdrawal delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); - - // Make bob not be able to receive value by setting his contract code to something without - // `receive` - vm.etch(address(bob), address(L1Token).code); - - vm.expectRevert(BondTransferFailed.selector); - gameProxy.claimCredit(address(bob)); - } -} - -/// @title FaultDisputeGame_CloseGame_Test -/// @notice Tests the closeGame functionality of the `FaultDisputeGame` contract. -contract FaultDisputeGameV2_CloseGame_Test is FaultDisputeGameV2_TestInit { - /// @notice Tests that closeGame reverts if the game is not resolved - function test_closeGame_gameNotResolved_reverts() public { - vm.expectRevert(GameNotResolved.selector); - gameProxy.closeGame(); - } - - /// @notice Tests that closeGame reverts if the game is paused - function test_closeGame_gamePaused_reverts() public { - // Pause the system with the Superchain-wide identifier (address(0)). - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(address(0)); - - // Attempting to close the game should now revert. - vm.expectRevert(GamePaused.selector); - gameProxy.closeGame(); - } - - /// @notice Tests that closeGame reverts if the game is not finalized - function test_closeGame_gameNotFinalized_reverts() public { - // Resolve the game - vm.warp(block.timestamp + 3 days + 12 hours); - gameProxy.resolveClaim(0, 0); - gameProxy.resolve(); - - // Don't wait the finalization delay - vm.expectRevert(GameNotFinalized.selector); - gameProxy.closeGame(); - } - - /// @notice Tests that closeGame succeeds for a proper game (normal distribution) - function test_closeGame_properGame_succeeds() public { - // Resolve the game - vm.warp(block.timestamp + 3 days + 12 hours); - gameProxy.resolveClaim(0, 0); - gameProxy.resolve(); - - // Wait for finalization delay - vm.warp(block.timestamp + 3.5 days + 1 seconds); - - // Close the game and verify normal distribution mode - vm.expectEmit(true, true, true, true); - emit GameClosed(BondDistributionMode.NORMAL); - gameProxy.closeGame(); - assertEq(uint8(gameProxy.bondDistributionMode()), uint8(BondDistributionMode.NORMAL)); - - // Check that the anchor state was set correctly. - assertEq(address(gameProxy.anchorStateRegistry().anchorGame()), address(gameProxy)); - } - - /// @notice Tests that closeGame succeeds for an improper game (refund mode) - function test_closeGame_improperGame_succeeds() public { - // Resolve the game - vm.warp(block.timestamp + 3 days + 12 hours); - gameProxy.resolveClaim(0, 0); - gameProxy.resolve(); - - // Wait for finalization delay - vm.warp(block.timestamp + 3.5 days + 1 seconds); - - // Mock the anchor registry to return improper game - vm.mockCall( - address(anchorStateRegistry), - abi.encodeCall(anchorStateRegistry.isGameProper, (IDisputeGame(address(gameProxy)))), - abi.encode(false, "") - ); - - // Close the game and verify refund mode - vm.expectEmit(true, true, true, true); - emit GameClosed(BondDistributionMode.REFUND); - gameProxy.closeGame(); - assertEq(uint8(gameProxy.bondDistributionMode()), uint8(BondDistributionMode.REFUND)); - } - - /// @notice Tests that multiple calls to closeGame succeed after initial distribution mode is - /// set - function test_closeGame_multipleCallsAfterSet_succeeds() public { - // Resolve and close the game first - vm.warp(block.timestamp + 3 days + 12 hours); - gameProxy.resolveClaim(0, 0); - gameProxy.resolve(); - - // Wait for finalization delay - vm.warp(block.timestamp + 3.5 days + 1 seconds); - - // First close sets the mode - gameProxy.closeGame(); - assertEq(uint8(gameProxy.bondDistributionMode()), uint8(BondDistributionMode.NORMAL)); - - // Subsequent closes should succeed without changing the mode - gameProxy.closeGame(); - assertEq(uint8(gameProxy.bondDistributionMode()), uint8(BondDistributionMode.NORMAL)); - - gameProxy.closeGame(); - assertEq(uint8(gameProxy.bondDistributionMode()), uint8(BondDistributionMode.NORMAL)); - } - - /// @notice Tests that closeGame called with any amount of gas either reverts (with OOG) or - /// updates the anchor state. This is specifically to verify that the try/catch inside - /// closeGame can't be called with just enough gas to OOG when calling the - /// AnchorStateRegistry but successfully execute the remainder of the function. - /// @param _gas Amount of gas to provide to closeGame. - function testFuzz_closeGame_canUpdateAnchorStateAndDoes_succeeds(uint256 _gas) public { - // Resolve and close the game first - vm.warp(block.timestamp + 3 days + 12 hours); - gameProxy.resolveClaim(0, 0); - gameProxy.resolve(); - - // Wait for finalization delay - vm.warp(block.timestamp + 3.5 days + 1 seconds); - - // Since providing *too* much gas isn't the issue here, bounding it to half the block gas - // limit is sufficient. We want to know that either (1) the function reverts or (2) the - // anchor state gets updated. If the function doesn't revert and the anchor state isn't - // updated then we have a problem. - _gas = bound(_gas, 0, block.gaslimit / 2); - - // The anchor state should not be the game proxy. - assert(address(gameProxy.anchorStateRegistry().anchorGame()) != address(gameProxy)); - - // Try closing the game. - try gameProxy.closeGame{ gas: _gas }() { - // If we got here, the function didn't revert, so the anchor state should have updated. - assert(address(gameProxy.anchorStateRegistry().anchorGame()) == address(gameProxy)); - } catch { - // Ok, function reverted. - } - } -} - -/// @title FaultDisputeGame_GetChallengerDuration_Test -/// @notice Tests the getChallengerDuration functionality and related resolution tests. -contract FaultDisputeGameV2_GetChallengerDuration_Test is FaultDisputeGameV2_TestInit { - /// @notice Tests that if the game is not in progress, querying of `getChallengerDuration` - /// reverts - function test_getChallengerDuration_gameNotInProgress_reverts() public { - // resolve the game - vm.warp(block.timestamp + gameProxy.maxClockDuration().raw()); - - gameProxy.resolveClaim(0, 0); - gameProxy.resolve(); - - vm.expectRevert(GameNotInProgress.selector); - gameProxy.getChallengerDuration(1); - } - - /// @notice Static unit test asserting that resolveClaim isn't possible if there's time left - /// for a counter. - function test_resolution_lastSecondDisputes_succeeds() public { - // The honest proposer created an honest root claim during setup - node 0 - - // Defender's turn - vm.warp(block.timestamp + 3.5 days - 1 seconds); - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: _getRequiredBond(0) }(disputed, 0, _dummyClaim()); - // Chess clock time accumulated: - assertEq(gameProxy.getChallengerDuration(0).raw(), 3.5 days - 1 seconds); - assertEq(gameProxy.getChallengerDuration(1).raw(), 0); - - // Advance time by 1 second, so that the root claim challenger clock is expired. - vm.warp(block.timestamp + 1 seconds); - // Attempt a second attack against the root claim. This should revert since the challenger - // clock is expired. - uint256 expectedBond = _getRequiredBond(0); - vm.expectRevert(ClockTimeExceeded.selector); - gameProxy.attack{ value: expectedBond }(disputed, 0, _dummyClaim()); - // Chess clock time accumulated: - assertEq(gameProxy.getChallengerDuration(0).raw(), 3.5 days); - assertEq(gameProxy.getChallengerDuration(1).raw(), 1 seconds); - - // Should not be able to resolve the root claim or second counter yet. - vm.expectRevert(ClockNotExpired.selector); - gameProxy.resolveClaim(1, 0); - vm.expectRevert(OutOfOrderResolution.selector); - gameProxy.resolveClaim(0, 0); - - // Warp to the last second of the root claim defender clock. - vm.warp(block.timestamp + 3.5 days - 2 seconds); - // Attack the challenge to the root claim. This should succeed, since the defender clock is - // not expired. - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.attack{ value: _getRequiredBond(1) }(disputed, 1, _dummyClaim()); - // Chess clock time accumulated: - assertEq(gameProxy.getChallengerDuration(0).raw(), 3.5 days); - assertEq(gameProxy.getChallengerDuration(1).raw(), 3.5 days - 1 seconds); - assertEq(gameProxy.getChallengerDuration(2).raw(), 3.5 days - gameProxy.clockExtension().raw()); - - // Should not be able to resolve any claims yet. - vm.expectRevert(ClockNotExpired.selector); - gameProxy.resolveClaim(2, 0); - vm.expectRevert(ClockNotExpired.selector); - gameProxy.resolveClaim(1, 0); - vm.expectRevert(OutOfOrderResolution.selector); - gameProxy.resolveClaim(0, 0); - - vm.warp(block.timestamp + gameProxy.clockExtension().raw() - 1 seconds); - - // Should not be able to resolve any claims yet. - vm.expectRevert(ClockNotExpired.selector); - gameProxy.resolveClaim(2, 0); - vm.expectRevert(OutOfOrderResolution.selector); - gameProxy.resolveClaim(1, 0); - vm.expectRevert(OutOfOrderResolution.selector); - gameProxy.resolveClaim(0, 0); - - // Chess clock time accumulated: - assertEq(gameProxy.getChallengerDuration(0).raw(), 3.5 days); - assertEq(gameProxy.getChallengerDuration(1).raw(), 3.5 days); - assertEq(gameProxy.getChallengerDuration(2).raw(), 3.5 days - 1 seconds); - - // Warp past the challenge period for the root claim defender. Defending the root claim - // should now revert. - vm.warp(block.timestamp + 1 seconds); - expectedBond = _getRequiredBond(1); - vm.expectRevert(ClockTimeExceeded.selector); // no further move can be made - gameProxy.attack{ value: expectedBond }(disputed, 1, _dummyClaim()); - expectedBond = _getRequiredBond(2); - (,,,, disputed,,) = gameProxy.claimData(2); - vm.expectRevert(ClockTimeExceeded.selector); // no further move can be made - gameProxy.attack{ value: expectedBond }(disputed, 2, _dummyClaim()); - // Chess clock time accumulated: - assertEq(gameProxy.getChallengerDuration(0).raw(), 3.5 days); - assertEq(gameProxy.getChallengerDuration(1).raw(), 3.5 days); - assertEq(gameProxy.getChallengerDuration(2).raw(), 3.5 days); - - vm.expectRevert(OutOfOrderResolution.selector); - gameProxy.resolveClaim(1, 0); - vm.expectRevert(OutOfOrderResolution.selector); - gameProxy.resolveClaim(0, 0); - - // All clocks are expired. Resolve the game. - gameProxy.resolveClaim(2, 0); // Node 2 is resolved as UNCOUNTERED by default since it has no children - gameProxy.resolveClaim(1, 0); // Node 1 is resolved as COUNTERED since it has an UNCOUNTERED child - gameProxy.resolveClaim(0, 0); // Node 0 is resolved as UNCOUNTERED since it has no UNCOUNTERED children - - // Defender wins game since the root claim is uncountered - assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); - } -} - -/// @title FaultDisputeGameV2_Uncategorized_Test -/// @notice General tests that are not testing any function directly of the `FaultDisputeGame` -/// contract or are testing multiple functions at once. -contract FaultDisputeGameV2_Uncategorized_Test is FaultDisputeGameV2_TestInit { - /// @notice Tests that the game's starting timestamp is set correctly. - function test_createdAt_succeeds() public view { - assertEq(gameProxy.createdAt().raw(), block.timestamp); - } - - /// @notice Tests that startingOutputRoot and it's getters are set correctly. - function test_startingOutputRootGetters_succeeds() public view { - (Hash root, uint256 l2BlockNumber) = gameProxy.startingOutputRoot(); - (Hash anchorRoot, uint256 anchorRootBlockNumber) = anchorStateRegistry.anchors(GAME_TYPE); - - assertEq(gameProxy.startingBlockNumber(), l2BlockNumber); - assertEq(gameProxy.startingBlockNumber(), anchorRootBlockNumber); - assertEq(Hash.unwrap(gameProxy.startingRootHash()), Hash.unwrap(root)); - assertEq(Hash.unwrap(gameProxy.startingRootHash()), Hash.unwrap(anchorRoot)); - } - - /// @notice Tests that the user cannot control the first 4 bytes of the CWIA data, disallowing - /// them to control the entrypoint when no calldata is provided to a call. - function test_cwiaCalldata_userCannotControlSelector_succeeds() public { - // Construct the expected CWIA data that the proxy will pass to the implementation, - // alongside any extra calldata passed by the user. - Hash l1Head = gameProxy.l1Head(); - bytes memory cwiaData = abi.encodePacked(address(this), gameProxy.rootClaim(), l1Head, gameProxy.extraData()); - - // We expect a `ReceiveETH` event to be emitted when 0 bytes of calldata are sent; The - // fallback is always reached *within the minimal proxy* in `LibClone`'s version of - // `clones-with-immutable-args` - vm.expectEmit(false, false, false, true); - emit ReceiveETH(0); - // We expect no delegatecall to the implementation contract if 0 bytes are sent. Assert - // that this happens 0 times. - vm.expectCall(address(gameImpl), cwiaData, 0); - (bool successA,) = address(gameProxy).call(hex""); - assertTrue(successA); - - // When calldata is forwarded, we do expect a delegatecall to the implementation. - bytes memory data = abi.encodePacked(gameProxy.l1Head.selector); - vm.expectCall(address(gameImpl), abi.encodePacked(data, cwiaData), 1); - (bool successB, bytes memory returnData) = address(gameProxy).call(data); - assertTrue(successB); - assertEq(returnData, abi.encode(l1Head)); - } -} - -contract FaultDispute_1v1_Actors_Test is FaultDisputeGameV2_TestInit { - /// @notice The honest actor - DisputeActor internal honest; - /// @notice The dishonest actor - DisputeActor internal dishonest; - - function setUp() public override { - // Setup the `FaultDisputeGame` - super.setUp(); - } - - /// @notice Fuzz test for a 1v1 output bisection dispute. - /// @notice The alphabet game has a constant status byte, and is not safe from someone being - /// dishonest in output bisection and then posting a correct execution trace bisection - /// root claim. This test does not cover this case (i.e. root claim of output bisection - /// is dishonest, root claim of execution trace bisection is made by the dishonest - /// actor but is honest, honest actor cannot attack it without risk of losing). - function testFuzz_outputBisection1v1honestRoot_succeeds(uint8 _divergeOutput, uint8 _divergeStep) public { - uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { - honestL2Outputs[i] = i + 1; - } - bytes memory honestTrace = new bytes(256); - for (uint256 i; i < honestTrace.length; i++) { - honestTrace[i] = bytes1(uint8(i)); - } - - uint256 divergeAtOutput = bound(_divergeOutput, 0, 15); - uint256 divergeAtStep = bound(_divergeStep, 0, 7); - uint256 divergeStepOffset = (divergeAtOutput << 4) + divergeAtStep; - - uint256[] memory dishonestL2Outputs = new uint256[](16); - for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i >= divergeAtOutput ? 0xFF : i + 1; - } - bytes memory dishonestTrace = new bytes(256); - for (uint256 i; i < dishonestTrace.length; i++) { - dishonestTrace[i] = i >= divergeStepOffset ? bytes1(uint8(0xFF)) : bytes1(uint8(i)); - } - - // Run the actor test - _actorTest({ - _rootClaim: 16, - _absolutePrestateData: 0, - _honestTrace: honestTrace, - _honestL2Outputs: honestL2Outputs, - _dishonestTrace: dishonestTrace, - _dishonestL2Outputs: dishonestL2Outputs, - _expectedStatus: GameStatus.DEFENDER_WINS - }); - } - - /// @notice Static unit test for a 1v1 output bisection dispute. - function test_static_1v1honestRootGenesisAbsolutePrestate_succeeds() public { - // The honest l2 outputs are from [1, 16] in this game. - uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { - honestL2Outputs[i] = i + 1; - } - // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, - // consisting of bytes [0, 255]. - bytes memory honestTrace = new bytes(256); - for (uint256 i; i < honestTrace.length; i++) { - honestTrace[i] = bytes1(uint8(i)); - } - - // The dishonest l2 outputs are from [2, 17] in this game. - uint256[] memory dishonestL2Outputs = new uint256[](16); - for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i + 2; - } - // The dishonest trace covers all block -> block + 1 transitions, and is 256 bytes long, - // consisting of all set bits. - bytes memory dishonestTrace = new bytes(256); - for (uint256 i; i < dishonestTrace.length; i++) { - dishonestTrace[i] = bytes1(0xFF); - } - - // Run the actor test - _actorTest({ - _rootClaim: 16, - _absolutePrestateData: 0, - _honestTrace: honestTrace, - _honestL2Outputs: honestL2Outputs, - _dishonestTrace: dishonestTrace, - _dishonestL2Outputs: dishonestL2Outputs, - _expectedStatus: GameStatus.DEFENDER_WINS - }); - } - - /// @notice Static unit test for a 1v1 output bisection dispute. - function test_static_1v1dishonestRootGenesisAbsolutePrestate_succeeds() public { - // The honest l2 outputs are from [1, 16] in this game. - uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { - honestL2Outputs[i] = i + 1; - } - // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, - // consisting of bytes [0, 255]. - bytes memory honestTrace = new bytes(256); - for (uint256 i; i < honestTrace.length; i++) { - honestTrace[i] = bytes1(uint8(i)); - } - - // The dishonest l2 outputs are from [2, 17] in this game. - uint256[] memory dishonestL2Outputs = new uint256[](16); - for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i + 2; - } - // The dishonest trace covers all block -> block + 1 transitions, and is 256 bytes long, consisting - // of all set bits. - bytes memory dishonestTrace = new bytes(256); - for (uint256 i; i < dishonestTrace.length; i++) { - dishonestTrace[i] = bytes1(0xFF); - } - - // Run the actor test - _actorTest({ - _rootClaim: 17, - _absolutePrestateData: 0, - _honestTrace: honestTrace, - _honestL2Outputs: honestL2Outputs, - _dishonestTrace: dishonestTrace, - _dishonestL2Outputs: dishonestL2Outputs, - _expectedStatus: GameStatus.CHALLENGER_WINS - }); - } - - /// @notice Static unit test for a 1v1 output bisection dispute. - function test_static_1v1honestRoot_succeeds() public { - // The honest l2 outputs are from [1, 16] in this game. - uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { - honestL2Outputs[i] = i + 1; - } - // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, consisting - // of bytes [0, 255]. - bytes memory honestTrace = new bytes(256); - for (uint256 i; i < honestTrace.length; i++) { - honestTrace[i] = bytes1(uint8(i)); - } - - // The dishonest l2 outputs are from [2, 17] in this game. - uint256[] memory dishonestL2Outputs = new uint256[](16); - for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i + 2; - } - // The dishonest trace covers all block -> block + 1 transitions, and is 256 bytes long, - // consisting of all zeros. - bytes memory dishonestTrace = new bytes(256); - - // Run the actor test - _actorTest({ - _rootClaim: 16, - _absolutePrestateData: 0, - _honestTrace: honestTrace, - _honestL2Outputs: honestL2Outputs, - _dishonestTrace: dishonestTrace, - _dishonestL2Outputs: dishonestL2Outputs, - _expectedStatus: GameStatus.DEFENDER_WINS - }); - } - - /// @notice Static unit test for a 1v1 output bisection dispute. - function test_static_1v1dishonestRoot_succeeds() public { - // The honest l2 outputs are from [1, 16] in this game. - uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { - honestL2Outputs[i] = i + 1; - } - // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, - // consisting of bytes [0, 255]. - bytes memory honestTrace = new bytes(256); - for (uint256 i; i < honestTrace.length; i++) { - honestTrace[i] = bytes1(uint8(i)); - } - - // The dishonest l2 outputs are from [2, 17] in this game. - uint256[] memory dishonestL2Outputs = new uint256[](16); - for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i + 2; - } - // The dishonest trace covers all block -> block + 1 transitions, and is 256 bytes long, - // consisting of all zeros. - bytes memory dishonestTrace = new bytes(256); - - // Run the actor test - _actorTest({ - _rootClaim: 17, - _absolutePrestateData: 0, - _honestTrace: honestTrace, - _honestL2Outputs: honestL2Outputs, - _dishonestTrace: dishonestTrace, - _dishonestL2Outputs: dishonestL2Outputs, - _expectedStatus: GameStatus.CHALLENGER_WINS - }); - } - - /// @notice Static unit test for a 1v1 output bisection dispute. - function test_static_1v1correctRootHalfWay_succeeds() public { - // The honest l2 outputs are from [1, 16] in this game. - uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { - honestL2Outputs[i] = i + 1; - } - // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, - // consisting of bytes [0, 255]. - bytes memory honestTrace = new bytes(256); - for (uint256 i; i < honestTrace.length; i++) { - honestTrace[i] = bytes1(uint8(i)); - } - - // The dishonest l2 outputs are half correct, half incorrect. - uint256[] memory dishonestL2Outputs = new uint256[](16); - for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; - } - // The dishonest trace is half correct, half incorrect. - bytes memory dishonestTrace = new bytes(256); - for (uint256 i; i < dishonestTrace.length; i++) { - dishonestTrace[i] = i > (127 + 4) ? bytes1(0xFF) : bytes1(uint8(i)); - } - - // Run the actor test - _actorTest({ - _rootClaim: 16, - _absolutePrestateData: 0, - _honestTrace: honestTrace, - _honestL2Outputs: honestL2Outputs, - _dishonestTrace: dishonestTrace, - _dishonestL2Outputs: dishonestL2Outputs, - _expectedStatus: GameStatus.DEFENDER_WINS - }); - } - - /// @notice Static unit test for a 1v1 output bisection dispute. - function test_static_1v1dishonestRootHalfWay_succeeds() public { - // The honest l2 outputs are from [1, 16] in this game. - uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { - honestL2Outputs[i] = i + 1; - } - // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, - // consisting of bytes [0, 255]. - bytes memory honestTrace = new bytes(256); - for (uint256 i; i < honestTrace.length; i++) { - honestTrace[i] = bytes1(uint8(i)); - } - - // The dishonest l2 outputs are half correct, half incorrect. - uint256[] memory dishonestL2Outputs = new uint256[](16); - for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; - } - // The dishonest trace is half correct, half incorrect. - bytes memory dishonestTrace = new bytes(256); - for (uint256 i; i < dishonestTrace.length; i++) { - dishonestTrace[i] = i > (127 + 4) ? bytes1(0xFF) : bytes1(uint8(i)); - } - - // Run the actor test - _actorTest({ - _rootClaim: 0xFF, - _absolutePrestateData: 0, - _honestTrace: honestTrace, - _honestL2Outputs: honestL2Outputs, - _dishonestTrace: dishonestTrace, - _dishonestL2Outputs: dishonestL2Outputs, - _expectedStatus: GameStatus.CHALLENGER_WINS - }); - } - - /// @notice Static unit test for a 1v1 output bisection dispute. - function test_static_1v1correctAbsolutePrestate_succeeds() public { - // The honest l2 outputs are from [1, 16] in this game. - uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { - honestL2Outputs[i] = i + 1; - } - // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, - // consisting of bytes [0, 255]. - bytes memory honestTrace = new bytes(256); - for (uint256 i; i < honestTrace.length; i++) { - honestTrace[i] = bytes1(uint8(i)); - } - - // The dishonest l2 outputs are half correct, half incorrect. - uint256[] memory dishonestL2Outputs = new uint256[](16); - for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; - } - // The dishonest trace correct is half correct, half incorrect. - bytes memory dishonestTrace = new bytes(256); - for (uint256 i; i < dishonestTrace.length; i++) { - dishonestTrace[i] = i > 127 ? bytes1(0xFF) : bytes1(uint8(i)); - } - - // Run the actor test - _actorTest({ - _rootClaim: 16, - _absolutePrestateData: 0, - _honestTrace: honestTrace, - _honestL2Outputs: honestL2Outputs, - _dishonestTrace: dishonestTrace, - _dishonestL2Outputs: dishonestL2Outputs, - _expectedStatus: GameStatus.DEFENDER_WINS - }); - } - - /// @notice Static unit test for a 1v1 output bisection dispute. - function test_static_1v1dishonestAbsolutePrestate_succeeds() public { - // The honest l2 outputs are from [1, 16] in this game. - uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { - honestL2Outputs[i] = i + 1; - } - // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, - // consisting of bytes [0, 255]. - bytes memory honestTrace = new bytes(256); - for (uint256 i; i < honestTrace.length; i++) { - honestTrace[i] = bytes1(uint8(i)); - } - - // The dishonest l2 outputs are half correct, half incorrect. - uint256[] memory dishonestL2Outputs = new uint256[](16); - for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; - } - // The dishonest trace correct is half correct, half incorrect. - bytes memory dishonestTrace = new bytes(256); - for (uint256 i; i < dishonestTrace.length; i++) { - dishonestTrace[i] = i > 127 ? bytes1(0xFF) : bytes1(uint8(i)); - } - - // Run the actor test - _actorTest({ - _rootClaim: 0xFF, - _absolutePrestateData: 0, - _honestTrace: honestTrace, - _honestL2Outputs: honestL2Outputs, - _dishonestTrace: dishonestTrace, - _dishonestL2Outputs: dishonestL2Outputs, - _expectedStatus: GameStatus.CHALLENGER_WINS - }); - } - - /// @notice Static unit test for a 1v1 output bisection dispute. - function test_static_1v1honestRootFinalInstruction_succeeds() public { - // The honest l2 outputs are from [1, 16] in this game. - uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { - honestL2Outputs[i] = i + 1; - } - // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, - // consisting of bytes [0, 255]. - bytes memory honestTrace = new bytes(256); - for (uint256 i; i < honestTrace.length; i++) { - honestTrace[i] = bytes1(uint8(i)); - } - - // The dishonest l2 outputs are half correct, half incorrect. - uint256[] memory dishonestL2Outputs = new uint256[](16); - for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; - } - // The dishonest trace is half correct, and correct all the way up to the final instruction - // of the exec subgame. - bytes memory dishonestTrace = new bytes(256); - for (uint256 i; i < dishonestTrace.length; i++) { - dishonestTrace[i] = i > (127 + 7) ? bytes1(0xFF) : bytes1(uint8(i)); - } - - // Run the actor test - _actorTest({ - _rootClaim: 16, - _absolutePrestateData: 0, - _honestTrace: honestTrace, - _honestL2Outputs: honestL2Outputs, - _dishonestTrace: dishonestTrace, - _dishonestL2Outputs: dishonestL2Outputs, - _expectedStatus: GameStatus.DEFENDER_WINS - }); - } - - /// @notice Static unit test for a 1v1 output bisection dispute. - function test_static_1v1dishonestRootFinalInstruction_succeeds() public { - // The honest l2 outputs are from [1, 16] in this game. - uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { - honestL2Outputs[i] = i + 1; - } - // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, - // consisting of bytes [0, 255]. - bytes memory honestTrace = new bytes(256); - for (uint256 i; i < honestTrace.length; i++) { - honestTrace[i] = bytes1(uint8(i)); - } - - // The dishonest l2 outputs are half correct, half incorrect. - uint256[] memory dishonestL2Outputs = new uint256[](16); - for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; - } - // The dishonest trace is half correct, and correct all the way up to the final instruction - // of the exec subgame. - bytes memory dishonestTrace = new bytes(256); - for (uint256 i; i < dishonestTrace.length; i++) { - dishonestTrace[i] = i > (127 + 7) ? bytes1(0xFF) : bytes1(uint8(i)); - } - - // Run the actor test - _actorTest({ - _rootClaim: 0xFF, - _absolutePrestateData: 0, - _honestTrace: honestTrace, - _honestL2Outputs: honestL2Outputs, - _dishonestTrace: dishonestTrace, - _dishonestL2Outputs: dishonestL2Outputs, - _expectedStatus: GameStatus.CHALLENGER_WINS - }); - } - - //////////////////////////////////////////////////////////////// - // HELPERS // - //////////////////////////////////////////////////////////////// - - /// @notice Helper to run a 1v1 actor test - function _actorTest( - uint256 _rootClaim, - uint256 _absolutePrestateData, - bytes memory _honestTrace, - uint256[] memory _honestL2Outputs, - bytes memory _dishonestTrace, - uint256[] memory _dishonestL2Outputs, - GameStatus _expectedStatus - ) - internal - { - if (isForkTest()) { - // Mock the call anchorStateRegistry.getAnchorRoot() to return 0 as the block number - (Hash root,) = anchorStateRegistry.getAnchorRoot(); - vm.mockCall( - address(anchorStateRegistry), - abi.encodeCall(IAnchorStateRegistry.getAnchorRoot, ()), - abi.encode(root, 0) - ); - } - - // Setup the environment - bytes memory absolutePrestateData = - _setup({ _absolutePrestateData: _absolutePrestateData, _rootClaim: _rootClaim }); - - // Create actors - _createActors({ - _honestTrace: _honestTrace, - _honestPreStateData: absolutePrestateData, - _honestL2Outputs: _honestL2Outputs, - _dishonestTrace: _dishonestTrace, - _dishonestPreStateData: absolutePrestateData, - _dishonestL2Outputs: _dishonestL2Outputs - }); - - // Exhaust all moves from both actors - _exhaustMoves(); - - // Resolve the game and assert that the defender won - _warpAndResolve(); - assertEq(uint8(gameProxy.status()), uint8(_expectedStatus)); - } - - /// @notice Helper to setup the 1v1 test - function _setup( - uint256 _absolutePrestateData, - uint256 _rootClaim - ) - internal - returns (bytes memory absolutePrestateData_) - { - absolutePrestateData_ = abi.encode(_absolutePrestateData); - Claim absolutePrestateExec = - _changeClaimStatus(Claim.wrap(keccak256(absolutePrestateData_)), VMStatuses.UNFINISHED); - Claim rootClaim = Claim.wrap(bytes32(uint256(_rootClaim))); - super.init({ rootClaim: rootClaim, absolutePrestate: absolutePrestateExec, l2BlockNumber: _rootClaim }); - } - - /// @notice Helper to create actors for the 1v1 dispute. - function _createActors( - bytes memory _honestTrace, - bytes memory _honestPreStateData, - uint256[] memory _honestL2Outputs, - bytes memory _dishonestTrace, - bytes memory _dishonestPreStateData, - uint256[] memory _dishonestL2Outputs - ) - internal - { - honest = new HonestDisputeActor({ - _gameProxy: IFaultDisputeGame(address(gameProxy)), - _l2Outputs: _honestL2Outputs, - _trace: _honestTrace, - _preStateData: _honestPreStateData - }); - dishonest = new HonestDisputeActor({ - _gameProxy: IFaultDisputeGame(address(gameProxy)), - _l2Outputs: _dishonestL2Outputs, - _trace: _dishonestTrace, - _preStateData: _dishonestPreStateData - }); - - vm.deal(address(honest), 100 ether); - vm.deal(address(dishonest), 100 ether); - vm.label(address(honest), "HonestActor"); - vm.label(address(dishonest), "DishonestActor"); - } - - /// @notice Helper to exhaust all moves from both actors. - function _exhaustMoves() internal { - while (true) { - // Allow the dishonest actor to make their moves, and then the honest actor. - (uint256 numMovesA,) = dishonest.move(); - (uint256 numMovesB, bool success) = honest.move(); - - require(success, "FaultDispute_1v1_Actors_Test: Honest actor's moves should always be successful"); - - // If both actors have run out of moves, we're done. - if (numMovesA == 0 && numMovesB == 0) break; - } - } - - /// @notice Helper to warp past the chess clock and resolve all claims within the dispute game. - function _warpAndResolve() internal { - // Warp past the chess clock - vm.warp(block.timestamp + 3 days + 12 hours); - - // Resolve all claims in reverse order. We allow `resolveClaim` calls to fail due to the - // check that prevents claims with no subgames attached from being passed to - // `resolveClaim`. There's also a check in `resolve` to ensure all children have been - // resolved before global resolution, which catches any unresolved subgames here. - for (uint256 i = gameProxy.claimDataLen(); i > 0; i--) { - (bool success,) = address(gameProxy).call(abi.encodeCall(gameProxy.resolveClaim, (i - 1, 0))); - assertTrue(success); - } - gameProxy.resolve(); - } -} diff --git a/packages/contracts-bedrock/test/dispute/v2/PermissionedDisputeGameV2.t.sol b/packages/contracts-bedrock/test/dispute/v2/PermissionedDisputeGameV2.t.sol deleted file mode 100644 index 1e621ac80389e..0000000000000 --- a/packages/contracts-bedrock/test/dispute/v2/PermissionedDisputeGameV2.t.sol +++ /dev/null @@ -1,417 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -// Testing -import { DisputeGameFactory_TestInit } from "test/dispute/DisputeGameFactory.t.sol"; -import { AlphabetVM } from "test/mocks/AlphabetVM.sol"; - -// Libraries -import "src/dispute/lib/Types.sol"; -import "src/dispute/lib/Errors.sol"; - -// Interfaces -import { IPermissionedDisputeGameV2 } from "interfaces/dispute/v2/IPermissionedDisputeGameV2.sol"; -import { IFaultDisputeGameV2 } from "interfaces/dispute/v2/IFaultDisputeGameV2.sol"; - -/// @title PermissionedDisputeGameV2_TestInit -/// @notice Reusable test initialization for `PermissionedDisputeGame` tests. -contract PermissionedDisputeGameV2_TestInit is DisputeGameFactory_TestInit { - /// @notice The type of the game being tested. - GameType internal immutable GAME_TYPE = GameTypes.PERMISSIONED_CANNON; - /// @notice Mock proposer key - address internal constant PROPOSER = address(0xfacade9); - /// @notice Mock challenger key - address internal constant CHALLENGER = address(0xfacadec); - - /// @dev The initial bond for the game. - uint256 internal initBond; - - /// @notice The implementation of the game. - IPermissionedDisputeGameV2 internal gameImpl; - /// @notice The `Clone` proxy of the game. - IPermissionedDisputeGameV2 internal gameProxy; - - /// @notice The extra data passed to the game for initialization. - bytes internal extraData; - - /// @notice The root claim of the game. - Claim internal rootClaim; - /// @notice An arbitrary root claim for testing. - Claim internal arbitaryRootClaim = Claim.wrap(bytes32(uint256(123))); - /// @notice Minimum bond value that covers all possible moves. - uint256 internal constant MIN_BOND = 50 ether; - - /// @notice The preimage of the absolute prestate claim - bytes internal absolutePrestateData; - /// @notice The absolute prestate of the trace. - Claim internal absolutePrestate; - /// @notice A valid l2BlockNumber that comes after the current anchor root block. - uint256 validL2BlockNumber; - - event Move(uint256 indexed parentIndex, Claim indexed pivot, address indexed claimant); - - function init(Claim _rootClaim, Claim _absolutePrestate, uint256 _l2BlockNumber) public { - // Set the time to a realistic date. - if (!isForkTest()) { - vm.warp(1690906994); - } - - // Fund the proposer on this fork. - vm.deal(PROPOSER, 100 ether); - - // Set the extra data for the game creation - extraData = abi.encode(_l2BlockNumber); - - (address _impl, AlphabetVM _vm,) = setupPermissionedDisputeGameV2(_absolutePrestate, PROPOSER, CHALLENGER); - gameImpl = IPermissionedDisputeGameV2(_impl); - - // Create a new game. - initBond = disputeGameFactory.initBonds(GAME_TYPE); - vm.mockCall( - address(anchorStateRegistry), - abi.encodeCall(anchorStateRegistry.anchors, (GAME_TYPE)), - abi.encode(_rootClaim, 0) - ); - vm.prank(PROPOSER, PROPOSER); - gameProxy = IPermissionedDisputeGameV2( - payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, _rootClaim, extraData))) - ); - - // Check immutables - assertEq(gameProxy.proposer(), PROPOSER); - assertEq(gameProxy.challenger(), CHALLENGER); - assertEq(gameProxy.gameType().raw(), GAME_TYPE.raw()); - assertEq(gameProxy.absolutePrestate().raw(), _absolutePrestate.raw()); - assertEq(gameProxy.maxGameDepth(), 2 ** 3); - assertEq(gameProxy.splitDepth(), 2 ** 2); - assertEq(gameProxy.clockExtension().raw(), 3 hours); - assertEq(gameProxy.maxClockDuration().raw(), 3.5 days); - assertEq(address(gameProxy.weth()), address(delayedWeth)); - assertEq(address(gameProxy.anchorStateRegistry()), address(anchorStateRegistry)); - assertEq(address(gameProxy.vm()), address(_vm)); - assertEq(address(gameProxy.gameCreator()), PROPOSER); - assertEq(gameProxy.l2ChainId(), l2ChainId); - - // Label the proxy - vm.label(address(gameProxy), "PermissionedDisputeGame_Clone"); - } - - function setUp() public override { - absolutePrestateData = abi.encode(0); - absolutePrestate = _changeClaimStatus(Claim.wrap(keccak256(absolutePrestateData)), VMStatuses.UNFINISHED); - - super.setUp(); - - // Get the actual anchor roots - (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - validL2BlockNumber = l2BlockNumber + 1; - rootClaim = Claim.wrap(Hash.unwrap(root)); - init({ _rootClaim: rootClaim, _absolutePrestate: absolutePrestate, _l2BlockNumber: validL2BlockNumber }); - } - - /// @dev Helper to return a pseudo-random claim - function _dummyClaim() internal view returns (Claim) { - return Claim.wrap(keccak256(abi.encode(gasleft()))); - } - - /// @dev Helper to get the required bond for the given claim index. - function _getRequiredBond(uint256 _claimIndex) internal view returns (uint256 bond_) { - (,,,,, Position parent,) = gameProxy.claimData(_claimIndex); - Position pos = parent.move(true); - bond_ = gameProxy.getRequiredBond(pos); - } - - /// @dev 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)) - } - } - - fallback() external payable { } - - receive() external payable { } - - function copyBytes(bytes memory src, bytes memory dest) internal pure returns (bytes memory) { - uint256 byteCount = src.length < dest.length ? src.length : dest.length; - for (uint256 i = 0; i < byteCount; i++) { - dest[i] = src[i]; - } - return dest; - } -} - -/// @title PermissionedDisputeGameV2_Version_Test -/// @notice Tests the `version` function of the `PermissionedDisputeGame` contract. -contract PermissionedDisputeGameV2_Version_Test is PermissionedDisputeGameV2_TestInit { - /// @notice Tests that the game's version function returns a string. - function test_version_works() public view { - assertTrue(bytes(gameProxy.version()).length > 0); - } -} - -/// @title PermissionedDisputeGameV2_Step_Test -/// @notice Tests the `step` function of the `PermissionedDisputeGame` contract. -contract PermissionedDisputeGameV2_Step_Test is PermissionedDisputeGameV2_TestInit { - /// @notice Tests that step works properly for the challenger. - function test_step_fromChallenger_succeeds() public { - validateStepForActor(CHALLENGER); - } - - /// @notice Tests that step works properly for the proposer. - function test_step_fromProposer_succeeds() public { - validateStepForActor(PROPOSER); - } - - function validateStepForActor(address actor) internal { - vm.deal(actor, 1_000 ether); - vm.startPrank(actor, actor); - - // Set up and perform the step - setupGameForStep(); - performStep(); - assertEq(gameProxy.claimDataLen(), 9); - - // Resolve the game and check that the expected actor countered the root claim - resolveGame(); - assertEq(uint256(gameProxy.status()), uint256(GameStatus.CHALLENGER_WINS)); - assertEq(gameProxy.resolvedAt().raw(), block.timestamp); - (, address counteredBy,,,,,) = gameProxy.claimData(0); - assertEq(counteredBy, actor); - - vm.stopPrank(); - } - - /// @notice Tests that step reverts for unauthorized addresses. - function test_step_notAuthorized_reverts(address _unauthorized) internal { - vm.assume(_unauthorized != PROPOSER && _unauthorized != CHALLENGER); - vm.deal(_unauthorized, 1_000 ether); - vm.deal(CHALLENGER, 1_000 ether); - - // Set up for the step using an authorized actor - vm.startPrank(CHALLENGER, CHALLENGER); - setupGameForStep(); - vm.stopPrank(); - - // Perform step with the unauthorized actor - vm.startPrank(_unauthorized, _unauthorized); - vm.expectRevert(BadAuth.selector); - performStep(); - - // Game should still be in progress, leaf claim should be missing - assertEq(uint256(gameProxy.status()), uint256(GameStatus.CHALLENGER_WINS)); - assertEq(gameProxy.claimDataLen(), 8); - - vm.stopPrank(); - } - - function setupGameForStep() internal { - // Make claims all the way down the tree. - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: _getRequiredBond(0) }(disputed, 0, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.attack{ value: _getRequiredBond(1) }(disputed, 1, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(2); - gameProxy.attack{ value: _getRequiredBond(2) }(disputed, 2, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(3); - gameProxy.attack{ value: _getRequiredBond(3) }(disputed, 3, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(4); - gameProxy.attack{ value: _getRequiredBond(4) }(disputed, 4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); - (,,,, disputed,,) = gameProxy.claimData(5); - gameProxy.attack{ value: _getRequiredBond(5) }(disputed, 5, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(6); - gameProxy.attack{ value: _getRequiredBond(6) }(disputed, 6, _dummyClaim()); - (,,,, disputed,,) = gameProxy.claimData(7); - gameProxy.attack{ value: _getRequiredBond(7) }(disputed, 7, _dummyClaim()); - - // Verify game state and add local data - assertEq(uint256(gameProxy.status()), uint256(GameStatus.IN_PROGRESS)); - gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); - } - - function performStep() internal { - gameProxy.step(8, true, absolutePrestateData, hex""); - } - - function resolveGame() internal { - vm.warp(block.timestamp + gameProxy.maxClockDuration().raw() + 1); - gameProxy.resolveClaim(8, 0); - gameProxy.resolveClaim(7, 0); - gameProxy.resolveClaim(6, 0); - gameProxy.resolveClaim(5, 0); - gameProxy.resolveClaim(4, 0); - gameProxy.resolveClaim(3, 0); - gameProxy.resolveClaim(2, 0); - gameProxy.resolveClaim(1, 0); - - gameProxy.resolveClaim(0, 0); - gameProxy.resolve(); - } -} - -/// @title PermissionedDisputeGame_Initialize_Test -/// @notice Tests the initialization of the `PermissionedDisputeGame` contract. -contract PermissionedDisputeGameV2_Initialize_Test is PermissionedDisputeGameV2_TestInit { - /// @notice Tests that the game cannot be initialized with incorrect CWIA calldata length - /// caused by extraData of the wrong length - function test_initialize_wrongExtradataLength_reverts(uint256 _extraDataLen) public { - // The `DisputeGameFactory` will pack the root claim and the extra data into a single - // array, which is enforced to be at least 64 bytes long. - // We bound the upper end to 23.5KB to ensure that the minimal proxy never surpasses the - // contract size limit in this test, as CWIA proxies store the immutable args in their - // bytecode. - // [0 bytes, 31 bytes] u [33 bytes, 23.5 KB] - _extraDataLen = bound(_extraDataLen, 0, 23_500); - if (_extraDataLen == 32) { - _extraDataLen++; - } - bytes memory _extraData = new bytes(_extraDataLen); - - // Assign the first 32 bytes in `extraData` to a valid L2 block number passed the starting - // block. - (, uint256 startingL2Block) = gameProxy.startingOutputRoot(); - assembly { - mstore(add(_extraData, 0x20), add(startingL2Block, 1)) - } - - Claim claim = _dummyClaim(); - vm.prank(PROPOSER, PROPOSER); - vm.expectRevert(IFaultDisputeGameV2.BadExtraData.selector); - gameProxy = IPermissionedDisputeGameV2( - payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, _extraData))) - ); - } - - /// @notice Tests that the game cannot be initialized with incorrect CWIA calldata length - /// caused by additional immutable args data - function test_initialize_extraImmutableArgsBytes_reverts(uint256 _extraByteCount) public { - (bytes memory correctArgs,,) = getPermissionedDisputeGameV2ImmutableArgs(absolutePrestate, PROPOSER, CHALLENGER); - - // We bound the upper end to 23.5KB to ensure that the minimal proxy never surpasses the - // contract size limit in this test, as CWIA proxies store the immutable args in their - // bytecode. - _extraByteCount = bound(_extraByteCount, 1, 23_500); - bytes memory immutableArgs = new bytes(_extraByteCount + correctArgs.length); - // Copy correct args into immutable args - copyBytes(correctArgs, immutableArgs); - - // Set up dispute game implementation with target immutableArgs - setupPermissionedDisputeGameV2(immutableArgs); - - Claim claim = _dummyClaim(); - vm.prank(PROPOSER, PROPOSER); - vm.expectRevert(IFaultDisputeGameV2.BadExtraData.selector); - gameProxy = IPermissionedDisputeGameV2( - payable( - address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, abi.encode(validL2BlockNumber))) - ) - ); - } - - /// @notice Tests that the game cannot be initialized with incorrect CWIA calldata length - /// caused by missing immutable args data - function test_initialize_missingImmutableArgsBytes_reverts(uint256 _truncatedByteCount) public { - (bytes memory correctArgs,,) = getPermissionedDisputeGameV2ImmutableArgs(absolutePrestate, PROPOSER, CHALLENGER); - - _truncatedByteCount = (_truncatedByteCount % correctArgs.length) + 1; - bytes memory immutableArgs = new bytes(correctArgs.length - _truncatedByteCount); - // Copy correct args into immutable args - copyBytes(correctArgs, immutableArgs); - - // Set up dispute game implementation with target immutableArgs - setupPermissionedDisputeGameV2(immutableArgs); - - Claim claim = _dummyClaim(); - vm.prank(PROPOSER, PROPOSER); - vm.expectRevert(IFaultDisputeGameV2.BadExtraData.selector); - gameProxy = IPermissionedDisputeGameV2( - payable( - address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, abi.encode(validL2BlockNumber))) - ) - ); - } -} - -/// @title PermissionedDisputeGameV2_Uncategorized_Test -/// @notice General tests that are not testing any function directly of the -/// `PermissionedDisputeGame` contract or are testing multiple functions at once. -contract PermissionedDisputeGameV2_Uncategorized_Test is PermissionedDisputeGameV2_TestInit { - /// @notice Tests that the proposer can create a permissioned dispute game. - function test_createGame_proposer_succeeds() public { - vm.prank(PROPOSER, PROPOSER); - disputeGameFactory.create{ value: initBond }(GAME_TYPE, arbitaryRootClaim, abi.encode(validL2BlockNumber)); - } - - /// @notice Tests that the permissioned game cannot be created by the challenger. - function test_createGame_challenger_reverts() public { - vm.deal(CHALLENGER, initBond); - vm.prank(CHALLENGER, CHALLENGER); - vm.expectRevert(BadAuth.selector); - disputeGameFactory.create{ value: initBond }(GAME_TYPE, arbitaryRootClaim, abi.encode(validL2BlockNumber)); - } - - /// @notice Tests that the permissioned game cannot be created by any address other than the - /// proposer. - function testFuzz_createGame_notProposer_reverts(address _p) public { - vm.assume(_p != PROPOSER); - - vm.deal(_p, initBond); - vm.prank(_p, _p); - vm.expectRevert(BadAuth.selector); - disputeGameFactory.create{ value: initBond }(GAME_TYPE, arbitaryRootClaim, abi.encode(validL2BlockNumber)); - } - - /// @notice Tests that the challenger can participate in a permissioned dispute game. - function test_participateInGame_challenger_succeeds() public { - vm.startPrank(CHALLENGER, CHALLENGER); - uint256 firstBond = _getRequiredBond(0); - vm.deal(CHALLENGER, firstBond); - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: firstBond }(disputed, 0, Claim.wrap(0)); - uint256 secondBond = _getRequiredBond(1); - vm.deal(CHALLENGER, secondBond); - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.defend{ value: secondBond }(disputed, 1, Claim.wrap(0)); - uint256 thirdBond = _getRequiredBond(2); - vm.deal(CHALLENGER, thirdBond); - (,,,, disputed,,) = gameProxy.claimData(2); - gameProxy.move{ value: thirdBond }(disputed, 2, Claim.wrap(0), true); - vm.stopPrank(); - } - - /// @notice Tests that the proposer can participate in a permissioned dispute game. - function test_participateInGame_proposer_succeeds() public { - vm.startPrank(PROPOSER, PROPOSER); - uint256 firstBond = _getRequiredBond(0); - vm.deal(PROPOSER, firstBond); - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: firstBond }(disputed, 0, Claim.wrap(0)); - uint256 secondBond = _getRequiredBond(1); - vm.deal(PROPOSER, secondBond); - (,,,, disputed,,) = gameProxy.claimData(1); - gameProxy.defend{ value: secondBond }(disputed, 1, Claim.wrap(0)); - uint256 thirdBond = _getRequiredBond(2); - vm.deal(PROPOSER, thirdBond); - (,,,, disputed,,) = gameProxy.claimData(2); - gameProxy.move{ value: thirdBond }(disputed, 2, Claim.wrap(0), true); - vm.stopPrank(); - } - - /// @notice Tests that addresses that are not the proposer or challenger cannot participate in - /// a permissioned dispute game. - function test_participateInGame_notAuthorized_reverts(address _p) public { - vm.assume(_p != PROPOSER && _p != CHALLENGER); - - vm.startPrank(_p, _p); - (,,,, Claim disputed,,) = gameProxy.claimData(0); - vm.expectRevert(BadAuth.selector); - gameProxy.attack(disputed, 0, Claim.wrap(0)); - vm.expectRevert(BadAuth.selector); - gameProxy.defend(disputed, 0, Claim.wrap(0)); - vm.expectRevert(BadAuth.selector); - gameProxy.move(disputed, 0, Claim.wrap(0), true); - vm.expectRevert(BadAuth.selector); - gameProxy.step(0, true, absolutePrestateData, hex""); - vm.stopPrank(); - } -} diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index 7b3753dc8f7fa..4c6c7667066bc 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -2,6 +2,8 @@ pragma solidity 0.8.15; import { Test } from "forge-std/Test.sol"; +import { FeatureFlags } from "test/setup/FeatureFlags.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; import { DeploySuperchain } from "scripts/deploy/DeploySuperchain.s.sol"; import { DeployImplementations } from "scripts/deploy/DeployImplementations.s.sol"; @@ -11,8 +13,9 @@ import { Types } from "scripts/libraries/Types.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { Claim, Duration, GameType, GameTypes } from "src/dispute/lib/Types.sol"; +import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; -contract DeployOPChain_TestBase is Test { +contract DeployOPChain_TestBase is Test, FeatureFlags { DeploySuperchain deploySuperchain; DeployImplementations deployImplementations; DeployOPChain deployOPChain; @@ -57,6 +60,7 @@ contract DeployOPChain_TestBase is Test { IOPContractsManager opcm; function setUp() public virtual { + resolveFeaturesFromEnv(); deploySuperchain = new DeploySuperchain(); deployImplementations = new DeployImplementations(); deployOPChain = new DeployOPChain(); @@ -91,7 +95,7 @@ contract DeployOPChain_TestBase is Test { superchainProxyAdmin: dso.superchainProxyAdmin, l1ProxyAdminOwner: dso.superchainProxyAdmin.owner(), challenger: challenger, - devFeatureBitmap: bytes32(0) + devFeatureBitmap: devFeatureBitmap }) ); opcm = dio.opcm; @@ -134,25 +138,26 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { // Basic non-zero and code checks are covered inside run->checkOutput. // Additonal targeted assertions added below. - assertEq(address(doo.permissionedDisputeGame.proposer()), proposer, "PDG proposer"); - assertEq(address(doo.permissionedDisputeGame.challenger()), challenger, "PDG challenger"); - assertEq(doo.permissionedDisputeGame.splitDepth(), disputeSplitDepth, "PDG splitDepth"); - assertEq(doo.permissionedDisputeGame.maxGameDepth(), disputeMaxGameDepth, "PDG maxGameDepth"); + IPermissionedDisputeGame pdg = getPermissionedDisputeGame(doo); + assertEq(pdg.splitDepth(), disputeSplitDepth, "PDG splitDepth"); + assertEq(pdg.maxGameDepth(), disputeMaxGameDepth, "PDG maxGameDepth"); + assertEq(Duration.unwrap(pdg.clockExtension()), Duration.unwrap(disputeClockExtension), "PDG clockExtension"); assertEq( - Duration.unwrap(doo.permissionedDisputeGame.clockExtension()), - Duration.unwrap(disputeClockExtension), - "PDG clockExtension" - ); - assertEq( - Duration.unwrap(doo.permissionedDisputeGame.maxClockDuration()), - Duration.unwrap(disputeMaxClockDuration), - "PDG maxClockDuration" - ); - assertEq( - Claim.unwrap(doo.permissionedDisputeGame.absolutePrestate()), - Claim.unwrap(disputeAbsolutePrestate), - "PDG absolutePrestate" + Duration.unwrap(pdg.maxClockDuration()), Duration.unwrap(disputeMaxClockDuration), "PDG maxClockDuration" ); + + if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { + // For v2 contracts, some immutable args are passed in at game creation time from DGF.gameArgs + assertEq(address(pdg.proposer()), address(0), "PDG proposer"); + assertEq(address(pdg.challenger()), address(0), "PDG challenger"); + assertEq(Claim.unwrap(pdg.absolutePrestate()), bytes32(0), "PDG absolutePrestate"); + } else { + assertEq(address(pdg.proposer()), proposer, "PDG proposer"); + assertEq(address(pdg.challenger()), challenger, "PDG challenger"); + assertEq( + Claim.unwrap(pdg.absolutePrestate()), Claim.unwrap(disputeAbsolutePrestate), "PDG absolutePrestate" + ); + } } function testFuzz_run_memory_succeeds(bytes32 _seed) public { @@ -172,22 +177,45 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON), 0, "2700"); assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.PERMISSIONED_CANNON), 0, "2800"); - assertEq(doo.permissionedDisputeGame.l2BlockNumber(), 0, "3000"); - assertEq( - Claim.unwrap(doo.permissionedDisputeGame.absolutePrestate()), - 0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c, - "3100" - ); - assertEq(Duration.unwrap(doo.permissionedDisputeGame.clockExtension()), 10800, "3200"); - assertEq(Duration.unwrap(doo.permissionedDisputeGame.maxClockDuration()), 302400, "3300"); - assertEq(doo.permissionedDisputeGame.splitDepth(), 30, "3400"); - assertEq(doo.permissionedDisputeGame.maxGameDepth(), 73, "3500"); + // Check dispute game deployments + // Validate permissionedDisputeGame (PDG) address + bool isDeployV2Games = isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + IOPContractsManager.Implementations memory impls = opcm.implementations(); + address expectedPDGAddress = + isDeployV2Games ? impls.permissionedDisputeGameV2Impl : address(doo.permissionedDisputeGame); + address actualPDGAddress = address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)); + assertNotEq(actualPDGAddress, address(0), "PDG address should be non-zero"); + assertEq(actualPDGAddress, expectedPDGAddress, "PDG address should match expected address"); + + // Check PDG getters + IPermissionedDisputeGame pdg = IPermissionedDisputeGame(actualPDGAddress); + bytes32 expectedPrestate = + isDeployV2Games ? bytes32(0) : bytes32(0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c); + assertEq(pdg.l2BlockNumber(), 0, "3000"); + assertEq(Claim.unwrap(pdg.absolutePrestate()), expectedPrestate, "3100"); + assertEq(Duration.unwrap(pdg.clockExtension()), 10800, "3200"); + assertEq(Duration.unwrap(pdg.maxClockDuration()), 302400, "3300"); + assertEq(pdg.splitDepth(), 30, "3400"); + assertEq(pdg.maxGameDepth(), 73, "3500"); } function test_customDisputeGame_customEnabled_succeeds() public { + // For v2 games, these parameters have already been configured at OPCM deploy time + skipIfDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + deployOPChainInput.allowCustomDisputeParameters = true; deployOPChainInput.disputeSplitDepth = disputeSplitDepth + 1; DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); - assertEq(doo.permissionedDisputeGame.splitDepth(), disputeSplitDepth + 1); + + IPermissionedDisputeGame pdg = getPermissionedDisputeGame(doo); + assertEq(pdg.splitDepth(), disputeSplitDepth + 1); + } + + function getPermissionedDisputeGame(DeployOPChain.Output memory doo) + internal + view + returns (IPermissionedDisputeGame) + { + return IPermissionedDisputeGame(address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON))); } } diff --git a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol index 0c355899ddf10..4090ab61fdd21 100644 --- a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol +++ b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.15; // Libraries import { LibString } from "@solady/utils/LibString.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Tests import { OPContractsManager_TestInit } from "test/L1/OPContractsManager.t.sol"; @@ -87,6 +88,27 @@ contract VerifyOPCM_Run_Test is VerifyOPCM_TestInit { harness.run(address(opcm), true); } + /// @notice Tests that the runSingle script succeeds when run against production contracts. + function test_runSingle_succeeds() public { + VerifyOPCM.OpcmContractRef[][2] memory refsByType; + refsByType[0] = harness.getOpcmContractRefs(opcm, "implementations", false); + refsByType[1] = harness.getOpcmContractRefs(opcm, "blueprints", true); + + for (uint8 i = 0; i < refsByType.length; i++) { + for (uint256 j = 0; j < refsByType[i].length; j++) { + VerifyOPCM.OpcmContractRef memory ref = refsByType[i][j]; + + // TODO(#17262): Remove these skips once these contracts are no longer behind a feature flag + // This script doesn't work for features that are in-development, so skip for now + if (_isDisputeGameV2ContractRef(ref)) { + continue; + } + + harness.runSingle(ref.name, ref.addr, true); + } + } + } + function test_run_bitmapNotEmptyOnMainnet_reverts(bytes32 _devFeatureBitmap) public { // Coverage changes bytecode and causes failures, skip. skipIfCoverage(); @@ -120,12 +142,21 @@ contract VerifyOPCM_Run_Test is VerifyOPCM_TestInit { // Grab the list of implementations. VerifyOPCM.OpcmContractRef[] memory refs = harness.getOpcmContractRefs(opcm, "implementations", false); + // Check if V2 dispute games feature is enabled + bytes32 bitmap = opcm.devFeatureBitmap(); + bool v2FeatureEnabled = DevFeatures.isDevFeatureEnabled(bitmap, DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + // Change 256 bytes at random. - for (uint8 i = 0; i < 255; i++) { + for (uint256 i = 0; i < 255; i++) { // Pick a random implementation to change. uint256 randomImplIndex = vm.randomUint(0, refs.length - 1); VerifyOPCM.OpcmContractRef memory ref = refs[randomImplIndex]; + // Skip V2 dispute games when feature disabled + if (_isDisputeGameV2ContractRef(ref) && !v2FeatureEnabled) { + continue; + } + // Get the code for the implementation. bytes memory implCode = ref.addr.code; @@ -180,12 +211,21 @@ contract VerifyOPCM_Run_Test is VerifyOPCM_TestInit { // Grab the list of implementations. VerifyOPCM.OpcmContractRef[] memory refs = harness.getOpcmContractRefs(opcm, "implementations", false); + // Check if V2 dispute games feature is enabled + bytes32 bitmap = opcm.devFeatureBitmap(); + bool v2FeatureEnabled = DevFeatures.isDevFeatureEnabled(bitmap, DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + // Change 256 bytes at random. for (uint8 i = 0; i < 255; i++) { // Pick a random implementation to change. uint256 randomImplIndex = vm.randomUint(0, refs.length - 1); VerifyOPCM.OpcmContractRef memory ref = refs[randomImplIndex]; + // Skip V2 dispute games when feature disabled + if (_isDisputeGameV2ContractRef(ref) && !v2FeatureEnabled) { + continue; + } + // Get the code for the implementation. bytes memory implCode = ref.addr.code; @@ -332,6 +372,10 @@ contract VerifyOPCM_Run_Test is VerifyOPCM_TestInit { assertGt(componentsWithContainerTested, 0, "Should have tested at least one component"); } + function _isDisputeGameV2ContractRef(VerifyOPCM.OpcmContractRef memory ref) internal pure returns (bool) { + return LibString.eq(ref.name, "FaultDisputeGameV2") || LibString.eq(ref.name, "PermissionedDisputeGameV2"); + } + /// @notice Utility function to mock the first OPCM component's contractsContainer address. /// @param _propRefs Array of property references to search through. /// @param _mockAddress The address to mock the contractsContainer call to return.