From f423dc5030623334c6436a166b4873fc67e4481c Mon Sep 17 00:00:00 2001 From: Zach Obront Date: Tue, 4 Jun 2024 16:14:45 -0700 Subject: [PATCH 01/13] optimistic zk contract --- .gitmodules | 3 + packages/contracts-bedrock/lib/sp1-contracts | 1 + packages/contracts-bedrock/remappings.txt | 22 + .../src/dispute/DisputeGameFactory.sol | 2 +- .../src/dispute/OptimisticZKGame.sol | 483 ++++++++++++++++++ .../dispute/interfaces/IOptimisticZKGame.sol | 44 ++ .../src/dispute/lib/Types.sol | 3 + 7 files changed, 557 insertions(+), 1 deletion(-) create mode 160000 packages/contracts-bedrock/lib/sp1-contracts create mode 100644 packages/contracts-bedrock/remappings.txt create mode 100644 packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol create mode 100644 packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol diff --git a/.gitmodules b/.gitmodules index f2b84783c4..583a085c31 100644 --- a/.gitmodules +++ b/.gitmodules @@ -26,3 +26,6 @@ [submodule "packages/contracts-bedrock/lib/automate"] path = packages/contracts-bedrock/lib/automate url = https://github.com/gelatodigital/automate +[submodule "packages/contracts-bedrock/lib/sp1-contracts"] + path = packages/contracts-bedrock/lib/sp1-contracts + url = https://github.com/succinctlabs/sp1-contracts diff --git a/packages/contracts-bedrock/lib/sp1-contracts b/packages/contracts-bedrock/lib/sp1-contracts new file mode 160000 index 0000000000..6103952070 --- /dev/null +++ b/packages/contracts-bedrock/lib/sp1-contracts @@ -0,0 +1 @@ +Subproject commit 610395207063bc8e38730280f93e8383e169412c diff --git a/packages/contracts-bedrock/remappings.txt b/packages/contracts-bedrock/remappings.txt new file mode 100644 index 0000000000..a702cb5e2c --- /dev/null +++ b/packages/contracts-bedrock/remappings.txt @@ -0,0 +1,22 @@ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@rari-capital/solmate/=lib/solmate/ +@lib-keccak/=lib/lib-keccak/contracts/lib/ +@solady/=lib/solady/src/ +forge-std/=lib/forge-std/src/ +ds-test/=lib/forge-std/lib/ds-test/src/ +safe-contracts/=lib/safe-contracts/contracts/ +kontrol-cheatcodes/=lib/kontrol-cheatcodes/src/ +gelato/=lib/automate/contracts/ +@solady-test/=lib/lib-keccak/lib/solady/test/ +automate/=lib/automate/contracts/ +erc4626-tests/=lib/automate/lib/openzeppelin-contracts/lib/erc4626-tests/ +hardhat/=lib/automate/node_modules/hardhat/ +lib-keccak/=lib/lib-keccak/contracts/ +openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/ +openzeppelin-contracts/=lib/openzeppelin-contracts/ +prb-test/=lib/automate/lib/prb-test/src/ +prb/-est/=lib/automate/lib/prb-test/src/ +solady/=lib/solady/ +solmate/=lib/solmate/src/ +@sp1-contracts/=lib/sp1-contracts/contracts/src/ diff --git a/packages/contracts-bedrock/src/dispute/DisputeGameFactory.sol b/packages/contracts-bedrock/src/dispute/DisputeGameFactory.sol index 55d25abb92..90248dfe42 100644 --- a/packages/contracts-bedrock/src/dispute/DisputeGameFactory.sol +++ b/packages/contracts-bedrock/src/dispute/DisputeGameFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.15; +pragma solidity ^0.8.15; import { LibClone } from "@solady/utils/LibClone.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; diff --git a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol new file mode 100644 index 0000000000..c7ba444af2 --- /dev/null +++ b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol @@ -0,0 +1,483 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { IDisputeGame } from "./interfaces/IDisputeGame.sol"; +import { IFaultDisputeGame } from "./interfaces/IFaultDisputeGame.sol"; +import { IDelayedWETH } from "./interfaces/IDelayedWETH.sol"; +import { IInitializable } from "./interfaces/IInitializable.sol"; +import { IOptimisticZKGame } from "./interfaces/IOptimisticZKGame.sol"; + +import { DisputeGameFactory } from "./DisputeGameFactory.sol"; +import { OutputRoot, GameStatus, Timestamp, Clock, Duration, GameType, Claim, Hash } from "src/dispute/lib/Types.sol"; +import { LibClock, LibDuration, LibTimestamp } from "./lib/LibUDT.sol"; + +import { Clone } from "@solady/utils/Clone.sol"; +import { SP1Verifier } from "@sp1-contracts/SP1Verifier.sol"; + +contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { + using LibClock for Clock; + using LibDuration for Duration; + using LibTimestamp for Timestamp; + + //////////////////////////////////////////////////////////////// + // State Vars // + //////////////////////////////////////////////////////////////// + + /// @notice An ID used for the proposer's initial bond, since it doesn't fall into a specific challenge. + uint constant GLOBAL_CHALLENGE_ID = type(uint64).max; + + /// @notice The DisputeGameFactory contract. + DisputeGameFactory immutable FACTORY; + + /// @notice The game type ID. + GameType immutable GAME_TYPE; + + /// @notice The maximum duration of the entire game. + /// @dev If the game has not resolved against the propoer by this time, the proposer wins. + Duration immutable MAX_GAME_DURATION; + + /// @notice The maximum duration of the sum of the proposer's turns in this game. + /// @dev If the proposer's clock runs out, the challenger wins the game. + Duration immutable MAX_PROPOSER_DURATION; + + /// @notice The DelayedWETH contract. + /// @dev Used as a temporary replacement for WETH to clawback bonds in the case of a fraudulent game. + IDelayedWETH immutable WETH; + + /// @notice The block number at which Ecotone was deployed. + /// @dev Blocks before this number cannot be used as a reference block for proving against. + uint immutable ECOTONE_ORIGIN_BLOCK; + + /// @notice The largest allowed gap between the reference block and the block being proved. + uint immutable MAX_BLOCK_GAP; + + /// @notice The timestamp at which the game was created. + Timestamp public createdAt; + + /// @notice The timestamp at which the game was resolved. + Timestamp public resolvedAt; + + /// @notice The previously confirmed root that the game is starting from. + OutputRoot anchorStateRoot; + + /// @notice The status of the game (in progress, challenger wins, or defender wins). + GameStatus public status; + + /// @notice The challenges being played on the game. + Challenge[] challenges; + + /// @notice Funds that have are pending withdrawal from the DelayedWETH contract. + mapping(address => uint) credits; + + //////////////////////////////////////////////////////////////// + // SETUP // + //////////////////////////////////////////////////////////////// + + /// @param _factory The DisputeGameFactory contract. + /// @param _gameType The game type ID. + /// @param _maxGameDuration The maximum duration of the entire game. + /// @param _maxProposerDuration The maximum duration of the sum of the proposer's turns in this game. + /// @param _weth The DelayedWETH contract. + /// @param _originBlock The earliest block number that can be proved against. + /// @param _maxBlockGap The largest allowed gap between the reference block and the block being proved. + constructor(address _factory, GameType _gameType, Duration _maxGameDuration, Duration _maxProposerDuration, IDelayedWETH _weth, uint _originBlock, uint _maxBlockGap) { + // The challenger's time will be equal to _maxGameDuration - _maxProposerDuration, so ensure that it is equal to the proposer's time. + if (_maxGameDuration.raw() != _maxProposerDuration.raw() * 2) revert InvalidDurations(); + + // Set all the immutable values in the implementation contract. + FACTORY = DisputeGameFactory(_factory); + GAME_TYPE = _gameType; + MAX_GAME_DURATION = _maxGameDuration; + MAX_PROPOSER_DURATION = _maxProposerDuration; + WETH = _weth; + ECOTONE_ORIGIN_BLOCK = _originBlock; + MAX_BLOCK_GAP = _maxBlockGap; + } + + /// @inheritdoc IInitializable + function initialize() external payable { + if (msg.sender != address(FACTORY)) revert Unauthorized(); + + // Query the factory to retrieve a game that has already been settled in favor of the defender. + (,, IDisputeGame prevGame) = FACTORY.gameAtIndex(startingRootIndex()); + if (prevGame.status() != GameStatus.DEFENDER_WINS) revert PreviousGameNotResolved(); + + // Pull the L2 block number from the previous game. + // @audit this is a hack because it's not fault dispute game. why isn't this is core interface? + uint prevL2BlockNumber = IFaultDisputeGame(address(prevGame)).l2BlockNumber(); + + // Set the anchorStateRoot to the previous game's root. + anchorStateRoot = OutputRoot({ root: Hash.wrap(prevGame.rootClaim().raw()), l2BlockNumber: prevL2BlockNumber }); + + // Only allow proving against blocks since Ecotone. + // This is because the state transition function used in the final ZK proof only computed Ecotone blocks. + if (anchorStateRoot.l2BlockNumber < ECOTONE_ORIGIN_BLOCK) revert InvalidBlockNumber(); + + // Require that the block being proven comes after the reference block. + if (anchorStateRoot.l2BlockNumber >= l2BlockNumber()) revert InvalidBlockNumber(); + + // Require that the gap between the reference block and the block being proven is less than MAX_BLOCK_GAP. + // This is used to avoid attacks where such a large gap is used that the game cannot be completed. + if (anchorStateRoot.l2BlockNumber + MAX_BLOCK_GAP < l2BlockNumber()) revert InvalidBlockNumber(); + + // Deposit the initial bond into the contract. + _depositBond(GLOBAL_CHALLENGE_ID); + + // Set the game's creation timestamp to the time of initialization. + createdAt = Timestamp.wrap(uint64(block.timestamp)); + } + + //////////////////////////////////////////////////////////////// + // BISECTION // + //////////////////////////////////////////////////////////////// + + function createNewChallenge(uint _blockNum, bytes32 _proposedRoot) public payable { + // The current length will be the ID after a new challenge is pushed to the array. + uint challengeId = challenges.length; + + // Don't allow proposer to challenge themselves. + // This is needed so that we can trust that the proposer address winning means the root is valid. + if (msg.sender == proposer()) revert ProposerIsChallenger(); + + // Create a new challenge. + challenges.push(Challenge({ + challenger: msg.sender, + proposerClock: LibClock.wrap(Duration.wrap(0), Timestamp.wrap(0)), + left: IntermediateRootClaim({ + outputRoot: anchorStateRoot, + status: IntermediateClaimStatus.ACCEPTED + }), + right: IntermediateRootClaim({ + outputRoot: OutputRoot({ root: Hash.wrap(rootClaim().raw()), l2BlockNumber: l2BlockNumber() }), + status: IntermediateClaimStatus.ACCEPTED + }), + current: OutputRoot({ root: Hash.wrap(bytes32(0)), l2BlockNumber: 0 }), + totalBonds: 0, + resolved: false + })); + + // Perform the first split on this new challenge. + split(challengeId, _blockNum, _proposedRoot); + } + + /// @param _blockNum The block number that the proposer is proposing a root for. + /// @param _proposedRoot The root that the proposer is proposing for the given block number. + function split(uint _challengeId, uint _blockNum, bytes32 _proposedRoot) public payable { + // Use the _challengeId to access the correct challenge struct. + if (_challengeId >= challenges.length) revert InvalidChallengeId(); + Challenge memory challenge = challenges[_challengeId]; + + // Only allow the challenge creator to act on this challenge. + if (msg.sender != challenge.challenger) revert Unauthorized(); + + // Only allow the challenger to call this function if it is their turn (ie the proposer timestamp is unset). + if (challenge.proposerClock.timestamp().raw() != 0) revert WrongTurn(); + + // Require this to be called at the bisection point between the left and right roots. + // Note that it is required for this to be an argument to avoid a frontrunning attack where `nextSplitBlock()` + // is shifted so that the challenger attests to the proposed root for the wrong block. + if (_blockNum != nextSplitBlock(_challengeId)) revert InvalidBlockNumber(); + + // If the left and right roots are already adjacent, `nextSplitBlock()` will return + // the left root. In this case, there is nothing to split and the game is ready to prove. + if (_blockNum == challenge.left.outputRoot.l2BlockNumber) revert ReadyToProve(); + + // Deposit the bond. + _depositBond(_challengeId); + + // Create a new bisection point for the proposal to respond to. + challenge.current = OutputRoot({ + l2BlockNumber: _blockNum, + root: Hash.wrap(_proposedRoot) + }); + + // Keep the proposer's new duration the same, and set the timestamp to now to start their turn. + challenge.proposerClock = LibClock.wrap(challenge.proposerClock.duration(), Timestamp.wrap(uint64(block.timestamp))); + + // Update the challenge in storage. + challenges[_challengeId] = challenge; + } + + /// @param _challengeId The ID of the challenge to respond to. + /// @param _accepted Whether the proposer accepts the proposed root or rejects it. + function respondToSplit(uint _challengeId, bool _accepted) public payable { + // Only allow the proposer to respond to splits. + if (msg.sender != proposer()) revert Unauthorized(); + + // Use the _challengeId to access the correct challenge struct. + if (_challengeId >= challenges.length) revert InvalidChallengeId(); + Challenge memory challenge = challenges[_challengeId]; + + // Only allow the proposer to call this function if it is their turn (ie their timestamp is set). + if (challenge.proposerClock.timestamp().raw() == 0) revert WrongTurn(); + + // Deposit the bond. + _depositBond(_challengeId); + + // If accepted, move the left root to the proposed root. + if (_accepted) { + challenge.left = IntermediateRootClaim({ + outputRoot: challenge.current, + status: IntermediateClaimStatus.ACCEPTED + }); + + // If disputed, move the right root to the proposed root. + } else { + challenge.right = IntermediateRootClaim({ + outputRoot: challenge.current, + status: IntermediateClaimStatus.CHALLENGED + }); + } + + // Calculate the proposer's new duration by incorporating any time since the latest timestamp. + // Set the proposer's timestamp to 0 to stop their turn. + Duration newDuration = totalClockDuration(challenge.proposerClock); + challenge.proposerClock = LibClock.wrap(newDuration, Timestamp.wrap(0)); + + // Update the challenge in storage. + challenges[_challengeId] = challenge; + } + + //////////////////////////////////////////////////////////////// + // RESOLUTION // + //////////////////////////////////////////////////////////////// + + function proveStep(uint _challengeId, bytes32 _vkey, bytes memory _proofBytes, bytes memory _blockTxData, bytes32 _alternateRoot) public payable { + // Use the _challengeId to access the correct challenge struct. + if (_challengeId >= challenges.length) revert InvalidChallengeId(); + Challenge memory challenge = challenges[_challengeId]; + + // Only allow the challenge creator to act on this challenge. + if (msg.sender != challenge.challenger) revert Unauthorized(); + + // Don't allow this to be called if (a) challenger has already won a different proof or (b) game has already resolved for proposer. + if (status != GameStatus.IN_PROGRESS) revert GameAlreadyResolved(); + + // Require that the left and right roots are adjacent and ready to prove. + if (challenge.left.outputRoot.l2BlockNumber + 1 != challenge.right.outputRoot.l2BlockNumber) revert NotReadyToProve(); + + // If the right root has been challenged by the proposer, the challenger must prove that we can transition from left to right. + if (challenge.right.status == IntermediateClaimStatus.CHALLENGED) { + // @todo how do we pull in the compressed tx data from blob? + bytes memory publicValues = abi.encode(challenge.left.outputRoot.root, challenge.right.outputRoot.root, _blockTxData); + verifyProof(_vkey, publicValues, _proofBytes); + + // If the right root is accepted, it means nothing has been challenged. + // The proposer is claiming that left (proposed block minus 1) transition to right (proposed block). + // The challenger must prove that left (proposed block minus 1) transitions to something else. + } else { + require(challenge.right.outputRoot.root.raw() != _alternateRoot, "need diff root to prove that left => !right"); + bytes memory publicValues = abi.encode(challenge.left.outputRoot.l2BlockNumber, _alternateRoot, _blockTxData); + verifyProof(_vkey, publicValues, _proofBytes); + } + + // Once the proof has been completed, resolve the game. + // The challenger who proves this step gets their own challenge's bond, plus the proposer's global bond. + uint[] memory _challengeIds = new uint[](2); + _challengeIds[0] = GLOBAL_CHALLENGE_ID; + _challengeIds[1] = _challengeId; + _resolveInternal(_challengeIds, msg.sender); + } + + /// @notice This function is called by the proposer to end the game if they were not successfully challenged. + // @audit this is a bit different from optimism functionality but matched it to match the interface + function resolve() public returns (GameStatus status_) { + // We can only resolve an in progress game where the full game clock has run out. + // If the proposer clock had run out or a ZK proof succeeded, the status would have been updated. + // This implies we are ready for the proposer to win the game. + if (status != GameStatus.IN_PROGRESS) revert GameAlreadyResolved(); + if (block.timestamp < createdAt.raw() + MAX_GAME_DURATION.raw()) revert ClockNotExpired(); + + // The proposer wins all games, so create an array of all of them (including global). + uint[] memory cIds = new uint[](challenges.length + 1); + for (uint i; i < challenges.length; i++) { + cIds[i] = i; + } + cIds[challenges.length] = GLOBAL_CHALLENGE_ID; + + // Resolve all challenges in favor of the proposer, and update the game status. + _resolveInternal(cIds, proposer()); + + // Return the resulting game status. + return status; + } + + /// @notice This function is called by the challenger to end the game if (a) the proposer's clock has run out + /// or (b) another challenge has already resolved in the challenger's favor. + function resolveChallenge(uint _challengeId) public { + // Use the _challengeId to access the correct challenge struct. + if (_challengeId >= challenges.length) revert InvalidChallengeId(); + Challenge memory challenge = challenges[_challengeId]; + + // Do not allow the same challenge to be resolved twice. + if (challenge.resolved) revert ChallengeAlreadyResolved(); + + // If this is the first challenge in which the proposer's clock has run out, + // resolve the game in favor of the challenger (and also reward them with the global bond). + if ( + totalClockDuration(challenge.proposerClock).raw() > MAX_PROPOSER_DURATION.raw() && + status == GameStatus.IN_PROGRESS + ) { + uint[] memory _challengeIds = new uint[](2); + _challengeIds[0] = GLOBAL_CHALLENGE_ID; + _challengeIds[1] = _challengeId; + _resolveInternal(_challengeIds, challenge.challenger); + + // If another challenge has resolved for the challenger, automatically allow the game to + // resolve in challenger's favor. + } else if (status == GameStatus.CHALLENGER_WINS) { + uint[] memory _challengeIds = new uint[](1); + _challengeIds[0] = _challengeId; + _resolveInternal(_challengeIds, challenge.challenger); + + // If the game is not resolved for the challenger and the clock is not expired, revert. + } else { + revert ChallengeCantBeResolved(); + } + } + + function _resolveInternal(uint[] memory _challengeIds, address _recipient) internal { + // Calculate the total amount to distribute to the recipient by summing the bonds of all challengeIds. + uint amountToDistribute; + for (uint i; i < _challengeIds.length; i++) { + uint cId = _challengeIds[i]; + amountToDistribute += _calculateBonds(cId); + + // Mark each challenge as resolved so it can't be called again. + if (cId != GLOBAL_CHALLENGE_ID) challenges[cId].resolved = true; + } + + // Request a delayed withdrawal from DelayedWETH and add withdrawal credits to the recipient. + credits[_recipient] += amountToDistribute; + WETH.unlock(_recipient, amountToDistribute); + + // If this is the first challenge being resolved, update the game status and resolvedAt timestamp. + if (status == GameStatus.IN_PROGRESS) { + status = _recipient == proposer() ? GameStatus.DEFENDER_WINS : GameStatus.CHALLENGER_WINS; + resolvedAt = Timestamp.wrap(uint64(block.timestamp)); + } + } + + //////////////////////////////////////////////////////////////// + // BONDS // + //////////////////////////////////////////////////////////////// + + function _depositBond(uint _challengeId) internal { + if (msg.value != getRequiredBond(_challengeId)) revert WrongBondAmount(); + if (_challengeId != GLOBAL_CHALLENGE_ID) challenges[_challengeId].totalBonds += msg.value; + WETH.deposit{value: msg.value}(); + } + + // Calculate the amount that should be distributed to the winner. + function _calculateBonds(uint _challengeId) internal returns (uint) { + if (_challengeId == GLOBAL_CHALLENGE_ID) return getRequiredBond(GLOBAL_CHALLENGE_ID); + + return challenges[_challengeId].totalBonds; + } + + /// @param _recipient The user to claim the ETH for. + function claimCredit(address _recipient) external { + // Cache the number of credits the user has and reset their local balance to 0. + uint256 recipientCredit = credits[_recipient]; + credits[_recipient] = 0; + + // Request the funds from the DelayedWETH contract. + // Note: This will revert if it has not been DELAY_SECONDS since WETH.unlock() was called. + WETH.withdraw(_recipient, recipientCredit); + + // Forward the funds to the recipient. + (bool success,) = _recipient.call{ value: recipientCredit }(hex""); + if (!success) revert TransferFailed(); + } + + //////////////////////////////////////////////////////////////// + // VIEWS // + //////////////////////////////////////////////////////////////// + + /// @param _challengeId The ID of the challenge to get the next split block for. + /// @return blockNum The block number that the proposer should propose a root for next. + function nextSplitBlock(uint _challengeId) public view returns (uint blockNum) { + Challenge memory challenge = challenges[_challengeId]; + + // If it's the proposer's turn, it returns the L2 block that they currently need to evaluate. + if (challenge.proposerClock.timestamp().raw() != 0) return challenge.current.l2BlockNumber; + + // If it's the challenger's turn, it bisects left and right to return the block they should next split on. + return (challenge.left.outputRoot.l2BlockNumber + challenge.right.outputRoot.l2BlockNumber) / 2; + } + + /// @param _clock The clock to calculate the total current duration for. + /// @return duration_ The total duration that the clock has been running for (including pending time on the current turn). + function totalClockDuration(Clock _clock) public view returns (Duration duration_) { + // If timestamp == 0, it's not the user's turn, so just return the duration. + if (_clock.timestamp().raw() == 0) { + duration_ = _clock.duration(); + + // Otherwise, add the time on the current turn to the duration to get the total. + } else { + uint timeOnCurrentMove = block.timestamp - _clock.timestamp().raw(); + duration_ = Duration.wrap(uint64(_clock.duration().raw() + timeOnCurrentMove)); + } + } + + /// @param _challengeId The ID of the challenge to get the required bond for. + /// @return requiredBond_ The amount of ETH required to bond for the given challenge. + /// @dev It is important that the sum of totalBonds on a challenge is enough to justify the ZK work. + /// Otherwise, the proposer could frontrun the proof to claim the larger bond back. + function getRequiredBond(uint _challengeId) public view returns (uint requiredBond_) { + // @todo think more about how to fairly calculate this so incentives are right + // keeping in mind that the proposer could frontrun all the disputes up to the proof? + // and that there might be games where there are no bisections + requiredBond_ = _challengeId == GLOBAL_CHALLENGE_ID ? 1 ether : 0.1 ether; + } + + /// @inheritdoc IDisputeGame + function gameType() public view override returns (GameType gameType_) { + gameType_ = GAME_TYPE; + } + + /// @inheritdoc IDisputeGame + function gameCreator() public pure returns (address creator_) { + creator_ = _getArgAddress(0x00); + } + + /// @inheritdoc IDisputeGame + function rootClaim() public pure returns (Claim rootClaim_) { + rootClaim_ = Claim.wrap(_getArgBytes32(0x14)); + } + + /// @inheritdoc IDisputeGame + function l1Head() public pure returns (Hash l1Head_) { + l1Head_ = Hash.wrap(_getArgBytes32(0x34)); + } + + /// @inheritdoc IDisputeGame + function extraData() public pure returns (bytes memory extraData_) { + // The extra data starts at the second word within the cwia calldata and + // is 32 bytes long. + extraData_ = _getArgBytes(0x54, 0x60); + } + + /// @return l2BlockNumber_ The block number that the game claiming is proving. + function l2BlockNumber() public pure returns (uint256 l2BlockNumber_) { + l2BlockNumber_ = _getArgUint256(0x54); + } + + + /// @return startingRootIndex_ The index of the validated game we are proving from. + function startingRootIndex() public pure returns (uint256 startingRootIndex_) { + startingRootIndex_ = _getArgUint256(0x74); + } + + /// @return proposerAddr_ The address of the proposer for this game. + function proposer() public pure returns (address proposerAddr_) { + proposerAddr_ = _getArgAddress(0x94); + } + + /// @inheritdoc IDisputeGame + function gameData() external view returns (GameType gameType_, Claim rootClaim_, bytes memory extraData_) { + gameType_ = gameType(); + rootClaim_ = rootClaim(); + extraData_ = extraData(); + } +} diff --git a/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol new file mode 100644 index 0000000000..48ecbb229c --- /dev/null +++ b/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { IDisputeGame } from "./IDisputeGame.sol"; +import "src/dispute/lib/Types.sol"; + +interface IOptimisticZKGame is IDisputeGame { + enum IntermediateClaimStatus { + NONE, + ACCEPTED, + CHALLENGED + } + + struct IntermediateRootClaim { + OutputRoot outputRoot; + IntermediateClaimStatus status; + } + + struct Challenge { + address challenger; + Clock proposerClock; + IntermediateRootClaim left; + IntermediateRootClaim right; + OutputRoot current; + uint totalBonds; + bool resolved; + } + + error Unauthorized(); + error ProposerIsChallenger(); + error InvalidDurations(); + error PreviousGameNotResolved(); + error InvalidBlockNumber(); + error WrongTurn(); + error ReadyToProve(); + error NotReadyToProve(); + error InvalidChallengeId(); + error GameAlreadyResolved(); + error ChallengeAlreadyResolved(); + error ChallengeCantBeResolved(); + error WrongBondAmount(); + error TransferFailed(); + error ClockNotExpired(); +} diff --git a/packages/contracts-bedrock/src/dispute/lib/Types.sol b/packages/contracts-bedrock/src/dispute/lib/Types.sol index 8d86e6e253..1b5340cd4f 100644 --- a/packages/contracts-bedrock/src/dispute/lib/Types.sol +++ b/packages/contracts-bedrock/src/dispute/lib/Types.sol @@ -33,6 +33,9 @@ library GameTypes { /// @notice A dispute game type the uses the asterisc VM GameType internal constant ASTERISC = GameType.wrap(2); + /// @notice A dispute game type that uses SP1 for Optimistic ZK proofs. + GameType internal constant OPTIMISTIC_SP1 = GameType.wrap(3); + /// @notice A dispute game type with short game duration for testing withdrawals. /// Not intended for production use. GameType internal constant FAST = GameType.wrap(254); From b2c6fbababfa17d1cb4559ccf7d0e4540ee90066 Mon Sep 17 00:00:00 2001 From: Zach Obront Date: Tue, 4 Jun 2024 16:28:03 -0700 Subject: [PATCH 02/13] req statement to revert --- packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol | 2 +- .../src/dispute/interfaces/IOptimisticZKGame.sol | 2 +- packages/contracts-bedrock/src/dispute/lib/Types.sol | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol index c7ba444af2..83b062737b 100644 --- a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol @@ -266,7 +266,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { // The proposer is claiming that left (proposed block minus 1) transition to right (proposed block). // The challenger must prove that left (proposed block minus 1) transitions to something else. } else { - require(challenge.right.outputRoot.root.raw() != _alternateRoot, "need diff root to prove that left => !right"); + if (challenge.right.outputRoot.root.raw() == _alternateRoot) revert InvalidRoot(); bytes memory publicValues = abi.encode(challenge.left.outputRoot.l2BlockNumber, _alternateRoot, _blockTxData); verifyProof(_vkey, publicValues, _proofBytes); } diff --git a/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol index 48ecbb229c..4512fb8c36 100644 --- a/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.15; import { IDisputeGame } from "./IDisputeGame.sol"; -import "src/dispute/lib/Types.sol"; interface IOptimisticZKGame is IDisputeGame { enum IntermediateClaimStatus { @@ -41,4 +40,5 @@ interface IOptimisticZKGame is IDisputeGame { error WrongBondAmount(); error TransferFailed(); error ClockNotExpired(); + error InvalidRoot(); } diff --git a/packages/contracts-bedrock/src/dispute/lib/Types.sol b/packages/contracts-bedrock/src/dispute/lib/Types.sol index 1b5340cd4f..6324bf7984 100644 --- a/packages/contracts-bedrock/src/dispute/lib/Types.sol +++ b/packages/contracts-bedrock/src/dispute/lib/Types.sol @@ -34,7 +34,7 @@ library GameTypes { GameType internal constant ASTERISC = GameType.wrap(2); /// @notice A dispute game type that uses SP1 for Optimistic ZK proofs. - GameType internal constant OPTIMISTIC_SP1 = GameType.wrap(3); + GameType internal constant OPTIMISTIC_ZK = GameType.wrap(3); /// @notice A dispute game type with short game duration for testing withdrawals. /// Not intended for production use. From 10e2068798b46327ad45c5d0a39fb2b77a7e31d4 Mon Sep 17 00:00:00 2001 From: Zach Obront Date: Tue, 4 Jun 2024 16:31:20 -0700 Subject: [PATCH 03/13] empty test file --- .../test/dispute/OptimisticZKGame.t.sol | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 packages/contracts-bedrock/test/dispute/OptimisticZKGame.t.sol diff --git a/packages/contracts-bedrock/test/dispute/OptimisticZKGame.t.sol b/packages/contracts-bedrock/test/dispute/OptimisticZKGame.t.sol new file mode 100644 index 0000000000..b720583157 --- /dev/null +++ b/packages/contracts-bedrock/test/dispute/OptimisticZKGame.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { Test, console } from "forge-std/Test.sol"; +import { OptimisticZKGame } from "src/dispute/OptimisticZKGame.sol"; +import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; + +contract OptimisticZKGameTest is Test { + + // function setUp() public { + // super.enableFaultProofs(); + // super.setUp(); + // DisputeGameFactory factory = new DisputeGameFactory(); + // factory.initialize(address(this)); + + // OptimisticZKGame gameImpl = new OptimisticZKGame({ + // _factory: address(factory), + // _gameType: GameType.OPTIMISTIC_ZK, + // _maxGameDuration: 6 days, + // _maxProposerDuration: 3 days, + // _weth: + // _originBlock: 0, + // _maxBlockGap: 52 weeks / 2 + // }); + + + // factory.setImplementation(GameType.OPTIMISTIC_ZK, address(gameImpl)); + // } +} From 477eb6c408ef6860ea07011fd630a593ac9152d2 Mon Sep 17 00:00:00 2001 From: Zach Obront Date: Tue, 4 Jun 2024 17:10:52 -0700 Subject: [PATCH 04/13] fix interface imports --- packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol | 4 ++-- .../src/dispute/interfaces/IOptimisticZKGame.sol | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol index 83b062737b..38719096d3 100644 --- a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol @@ -369,7 +369,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { } // Calculate the amount that should be distributed to the winner. - function _calculateBonds(uint _challengeId) internal returns (uint) { + function _calculateBonds(uint _challengeId) internal view returns (uint) { if (_challengeId == GLOBAL_CHALLENGE_ID) return getRequiredBond(GLOBAL_CHALLENGE_ID); return challenges[_challengeId].totalBonds; @@ -424,7 +424,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { /// @return requiredBond_ The amount of ETH required to bond for the given challenge. /// @dev It is important that the sum of totalBonds on a challenge is enough to justify the ZK work. /// Otherwise, the proposer could frontrun the proof to claim the larger bond back. - function getRequiredBond(uint _challengeId) public view returns (uint requiredBond_) { + function getRequiredBond(uint _challengeId) public pure returns (uint requiredBond_) { // @todo think more about how to fairly calculate this so incentives are right // keeping in mind that the proposer could frontrun all the disputes up to the proof? // and that there might be games where there are no bisections diff --git a/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol index 4512fb8c36..943eb9a880 100644 --- a/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.15; import { IDisputeGame } from "./IDisputeGame.sol"; +import "src/dispute/lib/Types.sol"; interface IOptimisticZKGame is IDisputeGame { enum IntermediateClaimStatus { From 6498f0703cbfbe0434b22a50991a79784260b679 Mon Sep 17 00:00:00 2001 From: Zach Obront Date: Tue, 4 Jun 2024 17:54:24 -0700 Subject: [PATCH 05/13] vkey immutable --- .../src/dispute/OptimisticZKGame.sol | 21 +- .../test/dispute/OptimisticZKGame.t.sol | 2370 ++++++++++++++++- 2 files changed, 2368 insertions(+), 23 deletions(-) diff --git a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol index 38719096d3..ed53e19adc 100644 --- a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol @@ -51,6 +51,9 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { /// @notice The largest allowed gap between the reference block and the block being proved. uint immutable MAX_BLOCK_GAP; + /// @notice The verification key used by the SP1Verifier contract. + bytes32 immutable VKEY; + /// @notice The timestamp at which the game was created. Timestamp public createdAt; @@ -80,7 +83,16 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { /// @param _weth The DelayedWETH contract. /// @param _originBlock The earliest block number that can be proved against. /// @param _maxBlockGap The largest allowed gap between the reference block and the block being proved. - constructor(address _factory, GameType _gameType, Duration _maxGameDuration, Duration _maxProposerDuration, IDelayedWETH _weth, uint _originBlock, uint _maxBlockGap) { + constructor( + address _factory, + GameType _gameType, + Duration _maxGameDuration, + Duration _maxProposerDuration, + IDelayedWETH _weth, + uint _originBlock, + uint _maxBlockGap, + bytes32 _vkey + ) { // The challenger's time will be equal to _maxGameDuration - _maxProposerDuration, so ensure that it is equal to the proposer's time. if (_maxGameDuration.raw() != _maxProposerDuration.raw() * 2) revert InvalidDurations(); @@ -92,6 +104,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { WETH = _weth; ECOTONE_ORIGIN_BLOCK = _originBlock; MAX_BLOCK_GAP = _maxBlockGap; + VKEY = _vkey; } /// @inheritdoc IInitializable @@ -242,7 +255,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { // RESOLUTION // //////////////////////////////////////////////////////////////// - function proveStep(uint _challengeId, bytes32 _vkey, bytes memory _proofBytes, bytes memory _blockTxData, bytes32 _alternateRoot) public payable { + function proveStep(uint _challengeId, bytes memory _proofBytes, bytes memory _blockTxData, bytes32 _alternateRoot) public payable { // Use the _challengeId to access the correct challenge struct. if (_challengeId >= challenges.length) revert InvalidChallengeId(); Challenge memory challenge = challenges[_challengeId]; @@ -260,7 +273,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { if (challenge.right.status == IntermediateClaimStatus.CHALLENGED) { // @todo how do we pull in the compressed tx data from blob? bytes memory publicValues = abi.encode(challenge.left.outputRoot.root, challenge.right.outputRoot.root, _blockTxData); - verifyProof(_vkey, publicValues, _proofBytes); + verifyProof(VKEY, publicValues, _proofBytes); // If the right root is accepted, it means nothing has been challenged. // The proposer is claiming that left (proposed block minus 1) transition to right (proposed block). @@ -268,7 +281,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { } else { if (challenge.right.outputRoot.root.raw() == _alternateRoot) revert InvalidRoot(); bytes memory publicValues = abi.encode(challenge.left.outputRoot.l2BlockNumber, _alternateRoot, _blockTxData); - verifyProof(_vkey, publicValues, _proofBytes); + verifyProof(VKEY, publicValues, _proofBytes); } // Once the proof has been completed, resolve the game. diff --git a/packages/contracts-bedrock/test/dispute/OptimisticZKGame.t.sol b/packages/contracts-bedrock/test/dispute/OptimisticZKGame.t.sol index b720583157..d03e84939b 100644 --- a/packages/contracts-bedrock/test/dispute/OptimisticZKGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/OptimisticZKGame.t.sol @@ -1,29 +1,2361 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; -import { Test, console } from "forge-std/Test.sol"; -import { OptimisticZKGame } from "src/dispute/OptimisticZKGame.sol"; +import { Test } from "forge-std/Test.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { DisputeGameFactory_Init } from "test/dispute/DisputeGameFactory.t.sol"; import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; +import { OptimisticZKGame } from "src/dispute/OptimisticZKGame.sol"; +import { DelayedWETH } from "src/dispute/weth/DelayedWETH.sol"; +import { PreimageOracle } from "src/cannon/PreimageOracle.sol"; + +import "src/dispute/lib/Types.sol"; +import "src/dispute/lib/Errors.sol"; +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 { IPreimageOracle } from "src/dispute/interfaces/IBigStepper.sol"; +import { IAnchorStateRegistry } from "src/dispute/interfaces/IAnchorStateRegistry.sol"; +import { AlphabetVM } from "test/mocks/AlphabetVM.sol"; + +import { DisputeActor, HonestDisputeActor } from "test/actors/FaultDisputeActors.sol"; + +contract OptimisticZKGame_Init is DisputeGameFactory_Init { + /// @dev The type of the game being tested. + GameType internal constant GAME_TYPE = GameType.wrap(3); + + /// @dev The implementation of the game. + OptimisticZKGame internal gameImpl; + /// @dev The `Clone` proxy of the game. + OptimisticZKGame internal gameProxy; + + /// @dev The extra data passed to the game for initialization. + bytes internal extraData; + + function init(Claim rootClaim, Claim absolutePrestate, uint256 l2BlockNumber) public { + // Set the time to a realistic date. + vm.warp(1690906994); + + // Set the extra data for the game creation + extraData = abi.encode(l2BlockNumber); + + AlphabetVM _vm = new AlphabetVM(absolutePrestate, new PreimageOracle(0, 0)); + + // Deploy an implementation of the fault game + gameImpl = new FaultDisputeGame({ + _gameType: GAME_TYPE, + _absolutePrestate: absolutePrestate, + _maxGameDepth: 2 ** 3, + _splitDepth: 2 ** 2, + _clockExtension: Duration.wrap(3 hours), + _maxClockDuration: Duration.wrap(3.5 days), + _vm: _vm, + _weth: delayedWeth, + _anchorStateRegistry: anchorStateRegistry, + _l2ChainId: 10 + }); + + // Register the game implementation with the factory. + disputeGameFactory.setImplementation(GAME_TYPE, gameImpl); + // Create a new game. + gameProxy = FaultDisputeGame(payable(address(disputeGameFactory.create(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)); + + // Label the proxy + vm.label(address(gameProxy), "FaultDisputeGame_Clone"); + } + + fallback() external payable { } + + receive() external payable { } +} + +contract FaultDisputeGame_Test is FaultDisputeGame_Init { + /// @dev The root claim of the game. + Claim internal constant ROOT_CLAIM = Claim.wrap(bytes32((uint256(1) << 248) | uint256(10))); + + /// @dev The preimage of the absolute prestate claim + bytes internal absolutePrestateData; + /// @dev The absolute prestate of the trace. + Claim internal absolutePrestate; + + function setUp() public override { + absolutePrestateData = abi.encode(0); + absolutePrestate = _changeClaimStatus(Claim.wrap(keccak256(absolutePrestateData)), VMStatuses.UNFINISHED); + + super.setUp(); + super.init({ rootClaim: ROOT_CLAIM, absolutePrestate: absolutePrestate, l2BlockNumber: 0x10 }); + } + + //////////////////////////////////////////////////////////////// + // `IDisputeGame` Implementation Tests // + //////////////////////////////////////////////////////////////// + + /// @dev 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 { + AlphabetVM alphabetVM = new AlphabetVM(absolutePrestate, new PreimageOracle(0, 0)); + + _maxGameDepth = bound(_maxGameDepth, LibPosition.MAX_POSITION_BITLEN, type(uint256).max - 1); + vm.expectRevert(MaxDepthTooLarge.selector); + new FaultDisputeGame({ + _gameType: GAME_TYPE, + _absolutePrestate: absolutePrestate, + _maxGameDepth: _maxGameDepth, + _splitDepth: _maxGameDepth + 1, + _clockExtension: Duration.wrap(3 hours), + _maxClockDuration: Duration.wrap(3.5 days), + _vm: alphabetVM, + _weth: DelayedWETH(payable(address(0))), + _anchorStateRegistry: IAnchorStateRegistry(address(0)), + _l2ChainId: 10 + }); + } + + /// @dev 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 { + AlphabetVM alphabetVM = new AlphabetVM(absolutePrestate, new PreimageOracle(0, 0)); + + _splitDepth = bound(_splitDepth, 2 ** 3, type(uint256).max); + vm.expectRevert(InvalidSplitDepth.selector); + new FaultDisputeGame({ + _gameType: GAME_TYPE, + _absolutePrestate: absolutePrestate, + _maxGameDepth: 2 ** 3, + _splitDepth: _splitDepth, + _clockExtension: Duration.wrap(3 hours), + _maxClockDuration: Duration.wrap(3.5 days), + _vm: alphabetVM, + _weth: DelayedWETH(payable(address(0))), + _anchorStateRegistry: IAnchorStateRegistry(address(0)), + _l2ChainId: 10 + }); + } + + /// @dev Tests that the constructor of the `FaultDisputeGame` reverts when clock extension is greater than the + /// max clock duration. + function testFuzz_constructor_clockExtensionTooLong_reverts( + uint64 _maxClockDuration, + uint64 _clockExtension + ) + public + { + AlphabetVM alphabetVM = new AlphabetVM(absolutePrestate, new PreimageOracle(0, 0)); + + _maxClockDuration = uint64(bound(_maxClockDuration, 0, type(uint64).max - 1)); + _clockExtension = uint64(bound(_clockExtension, _maxClockDuration + 1, type(uint64).max)); + vm.expectRevert(InvalidClockExtension.selector); + new FaultDisputeGame({ + _gameType: GAME_TYPE, + _absolutePrestate: absolutePrestate, + _maxGameDepth: 16, + _splitDepth: 8, + _clockExtension: Duration.wrap(_clockExtension), + _maxClockDuration: Duration.wrap(_maxClockDuration), + _vm: alphabetVM, + _weth: DelayedWETH(payable(address(0))), + _anchorStateRegistry: IAnchorStateRegistry(address(0)), + _l2ChainId: 10 + }); + } + + /// @dev Tests that the game's root claim is set correctly. + function test_rootClaim_succeeds() public view { + assertEq(gameProxy.rootClaim().raw(), ROOT_CLAIM.raw()); + } + + /// @dev Tests that the game's extra data is set correctly. + function test_extraData_succeeds() public view { + assertEq(gameProxy.extraData(), extraData); + } + + /// @dev Tests that the game's starting timestamp is set correctly. + function test_createdAt_succeeds() public view { + assertEq(gameProxy.createdAt().raw(), block.timestamp); + } + + /// @dev Tests that the game's type is set correctly. + function test_gameType_succeeds() public view { + assertEq(gameProxy.gameType().raw(), GAME_TYPE.raw()); + } + + /// @dev 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); + } + + //////////////////////////////////////////////////////////////// + // `IFaultDisputeGame` Implementation Tests // + //////////////////////////////////////////////////////////////// + + /// @dev 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 = + FaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, claim, abi.encode(_blockNumber))))); + } + + /// @dev 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 = FaultDisputeGame( + payable(address(disputeGameFactory.create{ value: _value }(GAME_TYPE, ROOT_CLAIM, abi.encode(1)))) + ); + assertEq(address(gameProxy).balance, 0); + assertEq(delayedWeth.balanceOf(address(gameProxy)), _value); + } + + /// @dev 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 { + // 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(abi.encodeWithSelector(BadExtraData.selector)); + gameProxy = FaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, claim, _extraData)))); + } + + /// @dev 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, 0); + 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)); + } + + /// @dev Tests that the game cannot be initialized twice. + function test_initialize_onlyOnce_succeeds() public { + vm.expectRevert(AlreadyInitialized.selector); + gameProxy.initialize(); + } + + /// @dev 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)); + } + + /// @dev 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); + } + } + + /// @dev 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); + } + + /// @dev 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)); + } + + /// @dev 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()); + } + + /// @dev 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(abi.encodeWithSignature("Panic(uint256)", 0x32)); + gameProxy.attack(_dummyClaim(), 1, claim); + + // Expect an out of bounds revert for a defense + vm.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x32)); + gameProxy.defend(_dummyClaim(), 1, claim); + } + + /// @dev 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); + } + } + } + + /// @dev 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 Static unit test that checks proper clock extension. + function test_move_clockExtensionCorrectness_succeeds() public { + (,,,,,, Clock clock) = gameProxy.claimData(0); + assertEq(clock.raw(), LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))).raw()); + + Claim claim = _dummyClaim(); + uint256 splitDepth = gameProxy.splitDepth(); + uint64 halfGameDuration = gameProxy.maxClockDuration().raw(); + uint64 clockExtension = gameProxy.clockExtension().raw(); + + // Make an initial attack against the root claim with 1 second left on the clock. The grandchild should be + // allocated exactly `clockExtension` seconds remaining on their potential clock. + vm.warp(block.timestamp + halfGameDuration - 1 seconds); + uint256 bond = _getRequiredBond(0); + (,,,, Claim disputed,,) = gameProxy.claimData(0); + gameProxy.attack{ value: bond }(disputed, 0, claim); + (,,,,,, clock) = gameProxy.claimData(1); + assertEq(clock.duration().raw(), halfGameDuration - clockExtension); + + // Warp ahead to the last second of the root claim defender's clock, and bisect all the way down to the move + // above the `SPLIT_DEPTH`. This warp guarantees that all moves from here on out will have clock extensions. + vm.warp(block.timestamp + halfGameDuration - 1 seconds); + for (uint256 i = 1; i < splitDepth - 2; i++) { + bond = _getRequiredBond(i); + (,,,, disputed,,) = gameProxy.claimData(i); + gameProxy.attack{ value: bond }(disputed, i, claim); + } + + // Warp ahead 1 seconds to have `clockExtension - 1 seconds` left on the next move's clock. + vm.warp(block.timestamp + 1 seconds); + + // The move above the split depth's grand child is the execution trace bisection root. The grandchild should + // be allocated `clockExtension * 2` seconds on their potential clock, if currently they have less than + // `clockExtension` seconds left. + bond = _getRequiredBond(splitDepth - 2); + (,,,, disputed,,) = gameProxy.claimData(splitDepth - 2); + gameProxy.attack{ value: bond }(disputed, splitDepth - 2, claim); + (,,,,,, clock) = gameProxy.claimData(splitDepth - 1); + assertEq(clock.duration().raw(), halfGameDuration - clockExtension * 2); + } + + /// @dev 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); + } + + /// @dev 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); + } + + /// @dev 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, 0); + 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()); + } + + /// @dev 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))); + } + + /// @dev 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)); + } + + /// @dev 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()); + } + + /// @dev 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()); + } + + /// @dev 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, 0, 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. + IDisputeGame game = disputeGameFactory.create(GAME_TYPE, Claim.wrap(outputRoot), abi.encode(_l2BlockNumber + 1)); + + // Challenge the L2 block number. + FaultDisputeGame fdg = FaultDisputeGame(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()); + } + + /// @dev 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, 0, 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; + IDisputeGame game = disputeGameFactory.create{ value: 0.1 ether }( + GAME_TYPE, Claim.wrap(outputRoot), abi.encode(_l2BlockNumber + 1) + ); + FaultDisputeGame fdg = FaultDisputeGame(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 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); + } + + /// @dev 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, 1, 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(GAME_TYPE, Claim.wrap(outputRoot), abi.encode(_l2BlockNumber)); + + // Challenge the L2 block number. + FaultDisputeGame fdg = FaultDisputeGame(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)); + } + + /// @dev 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""); + } + + /// @dev 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(GAME_TYPE, Claim.wrap(outputRoot), abi.encode(1)); + FaultDisputeGame fdg = FaultDisputeGame(address(game)); + + vm.expectRevert(InvalidHeaderRLP.selector); + fdg.challengeRootL2Block(outputRootProof, hex""); + } + + /// @dev 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(GAME_TYPE, Claim.wrap(outputRoot), abi.encode(1)); + FaultDisputeGame fdg = FaultDisputeGame(address(game)); + + vm.expectRevert(InvalidHeaderRLP.selector); + fdg.challengeRootL2Block(outputRootProof, hex""); + } + + /// @dev 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""); + } + + /// @dev 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""); + } + + /// @dev 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(gameImpl.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""); + } + + /// @dev 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(gameImpl.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""); + } + + /// @dev 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(gameImpl.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""); + } + + /// @dev 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)); + } + + /// @dev 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); + } + + /// @dev 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)); + } + + /// @dev 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(); + } + + /// @dev 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); + } + + /// @dev 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)); + } + + /// @dev 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)); + } + + /// @dev 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()); -contract OptimisticZKGameTest is Test { + vm.warp(block.timestamp + 3 days + 12 hours); - // function setUp() public { - // super.enableFaultProofs(); - // super.setUp(); - // DisputeGameFactory factory = new DisputeGameFactory(); - // factory.initialize(address(this)); + 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)); + } - // OptimisticZKGame gameImpl = new OptimisticZKGame({ - // _factory: address(factory), - // _gameType: GameType.OPTIMISTIC_ZK, - // _maxGameDuration: 6 days, - // _maxProposerDuration: 3 days, - // _weth: - // _originBlock: 0, - // _maxBlockGap: 52 weeks / 2 - // }); + /// @dev 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)); + } + + /// @dev 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); + + assertEq(address(this).balance, 0); + gameProxy.resolveClaim(2, 0); + gameProxy.resolveClaim(1, 0); + + // Wait for the withdrawal delay. + vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); + + gameProxy.claimCredit(address(this)); + assertEq(address(this).balance, firstBond + secondBond); + + vm.expectRevert(ClaimAlreadyResolved.selector); + gameProxy.resolveClaim(1, 0); + assertEq(address(this).balance, firstBond + secondBond); + } + + /// @dev 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); + + // Resolve to claim bond + uint256 balanceBefore = address(this).balance; + gameProxy.resolveClaim(8, 0); + + // Wait for the withdrawal delay. + vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); + + gameProxy.claimCredit(address(this)); + assertEq(address(this).balance, balanceBefore + _getRequiredBond(7)); + + vm.expectRevert(ClaimAlreadyResolved.selector); + gameProxy.resolveClaim(8, 0); + } + + /// @dev 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); + } + + /// @dev 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)), 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 the withdrawal delay. + vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); + + gameProxy.claimCredit(address(this)); + + // Ensure that bonds were paid out correctly. + assertEq(address(this).balance, bal); + 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), 0); + } + + /// @dev 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)), 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); + } + gameProxy.resolve(); + + // Wait for the withdrawal delay. + vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); + + 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 + 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), 0); + } + + /// @dev 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 the withdrawal delay. + vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); + + 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, "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), 0); + } + + /// @dev 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)); + + // 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()); + } + + /// @dev 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.encodeWithSelector(gameProxy.l2BlockNumber.selector), 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.l2BlockNumber()); + + // Resolve the game. + vm.mockCall(address(gameProxy), abi.encodeWithSelector(gameProxy.l2BlockNumber.selector), abi.encode(0)); + vm.warp(block.timestamp + 3 days + 12 hours); + gameProxy.resolveClaim(0, 0); + assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); + + // 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()); + } + + /// @dev 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. + 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)); + + // 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()); + } + + /// @dev 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)), 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); + + // 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)), 0); + + vm.stopPrank(); + } + + /// @dev 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); + } + + /// @dev 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(uint256(1) << 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); + } + } + + /// @dev 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(uint256(2) << 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); + } + } + + /// @dev 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)); + } + + /// @dev 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_); + } + + /// @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 return a pseudo-random claim + function _dummyClaim() internal view returns (Claim) { + return Claim.wrap(keccak256(abi.encode(gasleft()))); + } + + /// @dev 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)); + } +} + +contract FaultDispute_1v1_Actors_Test is FaultDisputeGame_Init { + /// @dev The honest actor + DisputeActor internal honest; + /// @dev The dishonest actor + DisputeActor internal dishonest; + + function setUp() public override { + // Setup the `FaultDisputeGame` + super.setUp(); + } + + /// @notice Fuzz test for a 1v1 output bisection dispute. + /// @dev 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 // + //////////////////////////////////////////////////////////////// + + /// @dev 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 + { + // 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)); + } + + /// @dev 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 }); + } + + /// @dev 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: gameProxy, + _l2Outputs: _honestL2Outputs, + _trace: _honestTrace, + _preStateData: _honestPreStateData + }); + dishonest = new HonestDisputeActor({ + _gameProxy: 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"); + } + + /// @dev 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, "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; + } + } + + /// @dev 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(); + } +} + +contract ClaimCreditReenter { + Vm internal immutable vm; + FaultDisputeGame internal immutable GAME; + uint256 public numCalls; + + constructor(FaultDisputeGame _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)); + } +} - // factory.setImplementation(GameType.OPTIMISTIC_ZK, address(gameImpl)); - // } +/// @dev 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)) + } } From 541ada1b954a269bc4a20d85f55df6b6285c77d1 Mon Sep 17 00:00:00 2001 From: Zach Obront Date: Tue, 4 Jun 2024 17:55:07 -0700 Subject: [PATCH 06/13] vkey immutable --- .../test/dispute/OptimisticZKGame.t.sol | 2361 ----------------- 1 file changed, 2361 deletions(-) delete mode 100644 packages/contracts-bedrock/test/dispute/OptimisticZKGame.t.sol diff --git a/packages/contracts-bedrock/test/dispute/OptimisticZKGame.t.sol b/packages/contracts-bedrock/test/dispute/OptimisticZKGame.t.sol deleted file mode 100644 index d03e84939b..0000000000 --- a/packages/contracts-bedrock/test/dispute/OptimisticZKGame.t.sol +++ /dev/null @@ -1,2361 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -import { Test } from "forge-std/Test.sol"; -import { Vm } from "forge-std/Vm.sol"; -import { DisputeGameFactory_Init } from "test/dispute/DisputeGameFactory.t.sol"; -import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; -import { OptimisticZKGame } from "src/dispute/OptimisticZKGame.sol"; -import { DelayedWETH } from "src/dispute/weth/DelayedWETH.sol"; -import { PreimageOracle } from "src/cannon/PreimageOracle.sol"; - -import "src/dispute/lib/Types.sol"; -import "src/dispute/lib/Errors.sol"; -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 { IPreimageOracle } from "src/dispute/interfaces/IBigStepper.sol"; -import { IAnchorStateRegistry } from "src/dispute/interfaces/IAnchorStateRegistry.sol"; -import { AlphabetVM } from "test/mocks/AlphabetVM.sol"; - -import { DisputeActor, HonestDisputeActor } from "test/actors/FaultDisputeActors.sol"; - -contract OptimisticZKGame_Init is DisputeGameFactory_Init { - /// @dev The type of the game being tested. - GameType internal constant GAME_TYPE = GameType.wrap(3); - - /// @dev The implementation of the game. - OptimisticZKGame internal gameImpl; - /// @dev The `Clone` proxy of the game. - OptimisticZKGame internal gameProxy; - - /// @dev The extra data passed to the game for initialization. - bytes internal extraData; - - function init(Claim rootClaim, Claim absolutePrestate, uint256 l2BlockNumber) public { - // Set the time to a realistic date. - vm.warp(1690906994); - - // Set the extra data for the game creation - extraData = abi.encode(l2BlockNumber); - - AlphabetVM _vm = new AlphabetVM(absolutePrestate, new PreimageOracle(0, 0)); - - // Deploy an implementation of the fault game - gameImpl = new FaultDisputeGame({ - _gameType: GAME_TYPE, - _absolutePrestate: absolutePrestate, - _maxGameDepth: 2 ** 3, - _splitDepth: 2 ** 2, - _clockExtension: Duration.wrap(3 hours), - _maxClockDuration: Duration.wrap(3.5 days), - _vm: _vm, - _weth: delayedWeth, - _anchorStateRegistry: anchorStateRegistry, - _l2ChainId: 10 - }); - - // Register the game implementation with the factory. - disputeGameFactory.setImplementation(GAME_TYPE, gameImpl); - // Create a new game. - gameProxy = FaultDisputeGame(payable(address(disputeGameFactory.create(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)); - - // Label the proxy - vm.label(address(gameProxy), "FaultDisputeGame_Clone"); - } - - fallback() external payable { } - - receive() external payable { } -} - -contract FaultDisputeGame_Test is FaultDisputeGame_Init { - /// @dev The root claim of the game. - Claim internal constant ROOT_CLAIM = Claim.wrap(bytes32((uint256(1) << 248) | uint256(10))); - - /// @dev The preimage of the absolute prestate claim - bytes internal absolutePrestateData; - /// @dev The absolute prestate of the trace. - Claim internal absolutePrestate; - - function setUp() public override { - absolutePrestateData = abi.encode(0); - absolutePrestate = _changeClaimStatus(Claim.wrap(keccak256(absolutePrestateData)), VMStatuses.UNFINISHED); - - super.setUp(); - super.init({ rootClaim: ROOT_CLAIM, absolutePrestate: absolutePrestate, l2BlockNumber: 0x10 }); - } - - //////////////////////////////////////////////////////////////// - // `IDisputeGame` Implementation Tests // - //////////////////////////////////////////////////////////////// - - /// @dev 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 { - AlphabetVM alphabetVM = new AlphabetVM(absolutePrestate, new PreimageOracle(0, 0)); - - _maxGameDepth = bound(_maxGameDepth, LibPosition.MAX_POSITION_BITLEN, type(uint256).max - 1); - vm.expectRevert(MaxDepthTooLarge.selector); - new FaultDisputeGame({ - _gameType: GAME_TYPE, - _absolutePrestate: absolutePrestate, - _maxGameDepth: _maxGameDepth, - _splitDepth: _maxGameDepth + 1, - _clockExtension: Duration.wrap(3 hours), - _maxClockDuration: Duration.wrap(3.5 days), - _vm: alphabetVM, - _weth: DelayedWETH(payable(address(0))), - _anchorStateRegistry: IAnchorStateRegistry(address(0)), - _l2ChainId: 10 - }); - } - - /// @dev 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 { - AlphabetVM alphabetVM = new AlphabetVM(absolutePrestate, new PreimageOracle(0, 0)); - - _splitDepth = bound(_splitDepth, 2 ** 3, type(uint256).max); - vm.expectRevert(InvalidSplitDepth.selector); - new FaultDisputeGame({ - _gameType: GAME_TYPE, - _absolutePrestate: absolutePrestate, - _maxGameDepth: 2 ** 3, - _splitDepth: _splitDepth, - _clockExtension: Duration.wrap(3 hours), - _maxClockDuration: Duration.wrap(3.5 days), - _vm: alphabetVM, - _weth: DelayedWETH(payable(address(0))), - _anchorStateRegistry: IAnchorStateRegistry(address(0)), - _l2ChainId: 10 - }); - } - - /// @dev Tests that the constructor of the `FaultDisputeGame` reverts when clock extension is greater than the - /// max clock duration. - function testFuzz_constructor_clockExtensionTooLong_reverts( - uint64 _maxClockDuration, - uint64 _clockExtension - ) - public - { - AlphabetVM alphabetVM = new AlphabetVM(absolutePrestate, new PreimageOracle(0, 0)); - - _maxClockDuration = uint64(bound(_maxClockDuration, 0, type(uint64).max - 1)); - _clockExtension = uint64(bound(_clockExtension, _maxClockDuration + 1, type(uint64).max)); - vm.expectRevert(InvalidClockExtension.selector); - new FaultDisputeGame({ - _gameType: GAME_TYPE, - _absolutePrestate: absolutePrestate, - _maxGameDepth: 16, - _splitDepth: 8, - _clockExtension: Duration.wrap(_clockExtension), - _maxClockDuration: Duration.wrap(_maxClockDuration), - _vm: alphabetVM, - _weth: DelayedWETH(payable(address(0))), - _anchorStateRegistry: IAnchorStateRegistry(address(0)), - _l2ChainId: 10 - }); - } - - /// @dev Tests that the game's root claim is set correctly. - function test_rootClaim_succeeds() public view { - assertEq(gameProxy.rootClaim().raw(), ROOT_CLAIM.raw()); - } - - /// @dev Tests that the game's extra data is set correctly. - function test_extraData_succeeds() public view { - assertEq(gameProxy.extraData(), extraData); - } - - /// @dev Tests that the game's starting timestamp is set correctly. - function test_createdAt_succeeds() public view { - assertEq(gameProxy.createdAt().raw(), block.timestamp); - } - - /// @dev Tests that the game's type is set correctly. - function test_gameType_succeeds() public view { - assertEq(gameProxy.gameType().raw(), GAME_TYPE.raw()); - } - - /// @dev 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); - } - - //////////////////////////////////////////////////////////////// - // `IFaultDisputeGame` Implementation Tests // - //////////////////////////////////////////////////////////////// - - /// @dev 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 = - FaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, claim, abi.encode(_blockNumber))))); - } - - /// @dev 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 = FaultDisputeGame( - payable(address(disputeGameFactory.create{ value: _value }(GAME_TYPE, ROOT_CLAIM, abi.encode(1)))) - ); - assertEq(address(gameProxy).balance, 0); - assertEq(delayedWeth.balanceOf(address(gameProxy)), _value); - } - - /// @dev 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 { - // 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(abi.encodeWithSelector(BadExtraData.selector)); - gameProxy = FaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, claim, _extraData)))); - } - - /// @dev 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, 0); - 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)); - } - - /// @dev Tests that the game cannot be initialized twice. - function test_initialize_onlyOnce_succeeds() public { - vm.expectRevert(AlreadyInitialized.selector); - gameProxy.initialize(); - } - - /// @dev 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)); - } - - /// @dev 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); - } - } - - /// @dev 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); - } - - /// @dev 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)); - } - - /// @dev 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()); - } - - /// @dev 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(abi.encodeWithSignature("Panic(uint256)", 0x32)); - gameProxy.attack(_dummyClaim(), 1, claim); - - // Expect an out of bounds revert for a defense - vm.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x32)); - gameProxy.defend(_dummyClaim(), 1, claim); - } - - /// @dev 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); - } - } - } - - /// @dev 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 Static unit test that checks proper clock extension. - function test_move_clockExtensionCorrectness_succeeds() public { - (,,,,,, Clock clock) = gameProxy.claimData(0); - assertEq(clock.raw(), LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))).raw()); - - Claim claim = _dummyClaim(); - uint256 splitDepth = gameProxy.splitDepth(); - uint64 halfGameDuration = gameProxy.maxClockDuration().raw(); - uint64 clockExtension = gameProxy.clockExtension().raw(); - - // Make an initial attack against the root claim with 1 second left on the clock. The grandchild should be - // allocated exactly `clockExtension` seconds remaining on their potential clock. - vm.warp(block.timestamp + halfGameDuration - 1 seconds); - uint256 bond = _getRequiredBond(0); - (,,,, Claim disputed,,) = gameProxy.claimData(0); - gameProxy.attack{ value: bond }(disputed, 0, claim); - (,,,,,, clock) = gameProxy.claimData(1); - assertEq(clock.duration().raw(), halfGameDuration - clockExtension); - - // Warp ahead to the last second of the root claim defender's clock, and bisect all the way down to the move - // above the `SPLIT_DEPTH`. This warp guarantees that all moves from here on out will have clock extensions. - vm.warp(block.timestamp + halfGameDuration - 1 seconds); - for (uint256 i = 1; i < splitDepth - 2; i++) { - bond = _getRequiredBond(i); - (,,,, disputed,,) = gameProxy.claimData(i); - gameProxy.attack{ value: bond }(disputed, i, claim); - } - - // Warp ahead 1 seconds to have `clockExtension - 1 seconds` left on the next move's clock. - vm.warp(block.timestamp + 1 seconds); - - // The move above the split depth's grand child is the execution trace bisection root. The grandchild should - // be allocated `clockExtension * 2` seconds on their potential clock, if currently they have less than - // `clockExtension` seconds left. - bond = _getRequiredBond(splitDepth - 2); - (,,,, disputed,,) = gameProxy.claimData(splitDepth - 2); - gameProxy.attack{ value: bond }(disputed, splitDepth - 2, claim); - (,,,,,, clock) = gameProxy.claimData(splitDepth - 1); - assertEq(clock.duration().raw(), halfGameDuration - clockExtension * 2); - } - - /// @dev 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); - } - - /// @dev 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); - } - - /// @dev 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, 0); - 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()); - } - - /// @dev 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))); - } - - /// @dev 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)); - } - - /// @dev 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()); - } - - /// @dev 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()); - } - - /// @dev 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, 0, 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. - IDisputeGame game = disputeGameFactory.create(GAME_TYPE, Claim.wrap(outputRoot), abi.encode(_l2BlockNumber + 1)); - - // Challenge the L2 block number. - FaultDisputeGame fdg = FaultDisputeGame(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()); - } - - /// @dev 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, 0, 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; - IDisputeGame game = disputeGameFactory.create{ value: 0.1 ether }( - GAME_TYPE, Claim.wrap(outputRoot), abi.encode(_l2BlockNumber + 1) - ); - FaultDisputeGame fdg = FaultDisputeGame(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 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); - } - - /// @dev 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, 1, 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(GAME_TYPE, Claim.wrap(outputRoot), abi.encode(_l2BlockNumber)); - - // Challenge the L2 block number. - FaultDisputeGame fdg = FaultDisputeGame(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)); - } - - /// @dev 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""); - } - - /// @dev 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(GAME_TYPE, Claim.wrap(outputRoot), abi.encode(1)); - FaultDisputeGame fdg = FaultDisputeGame(address(game)); - - vm.expectRevert(InvalidHeaderRLP.selector); - fdg.challengeRootL2Block(outputRootProof, hex""); - } - - /// @dev 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(GAME_TYPE, Claim.wrap(outputRoot), abi.encode(1)); - FaultDisputeGame fdg = FaultDisputeGame(address(game)); - - vm.expectRevert(InvalidHeaderRLP.selector); - fdg.challengeRootL2Block(outputRootProof, hex""); - } - - /// @dev 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""); - } - - /// @dev 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""); - } - - /// @dev 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(gameImpl.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""); - } - - /// @dev 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(gameImpl.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""); - } - - /// @dev 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(gameImpl.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""); - } - - /// @dev 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)); - } - - /// @dev 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); - } - - /// @dev 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)); - } - - /// @dev 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(); - } - - /// @dev 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); - } - - /// @dev 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)); - } - - /// @dev 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)); - } - - /// @dev 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)); - } - - /// @dev 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)); - } - - /// @dev 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); - - assertEq(address(this).balance, 0); - gameProxy.resolveClaim(2, 0); - gameProxy.resolveClaim(1, 0); - - // Wait for the withdrawal delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); - - gameProxy.claimCredit(address(this)); - assertEq(address(this).balance, firstBond + secondBond); - - vm.expectRevert(ClaimAlreadyResolved.selector); - gameProxy.resolveClaim(1, 0); - assertEq(address(this).balance, firstBond + secondBond); - } - - /// @dev 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); - - // Resolve to claim bond - uint256 balanceBefore = address(this).balance; - gameProxy.resolveClaim(8, 0); - - // Wait for the withdrawal delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); - - gameProxy.claimCredit(address(this)); - assertEq(address(this).balance, balanceBefore + _getRequiredBond(7)); - - vm.expectRevert(ClaimAlreadyResolved.selector); - gameProxy.resolveClaim(8, 0); - } - - /// @dev 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); - } - - /// @dev 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)), 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 the withdrawal delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); - - gameProxy.claimCredit(address(this)); - - // Ensure that bonds were paid out correctly. - assertEq(address(this).balance, bal); - 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), 0); - } - - /// @dev 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)), 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); - } - gameProxy.resolve(); - - // Wait for the withdrawal delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); - - 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 + 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), 0); - } - - /// @dev 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 the withdrawal delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); - - 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, "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), 0); - } - - /// @dev 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)); - - // 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()); - } - - /// @dev 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.encodeWithSelector(gameProxy.l2BlockNumber.selector), 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.l2BlockNumber()); - - // Resolve the game. - vm.mockCall(address(gameProxy), abi.encodeWithSelector(gameProxy.l2BlockNumber.selector), abi.encode(0)); - vm.warp(block.timestamp + 3 days + 12 hours); - gameProxy.resolveClaim(0, 0); - assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); - - // 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()); - } - - /// @dev 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. - 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)); - - // 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()); - } - - /// @dev 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)), 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); - - // 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)), 0); - - vm.stopPrank(); - } - - /// @dev 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); - } - - /// @dev 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(uint256(1) << 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); - } - } - - /// @dev 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(uint256(2) << 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); - } - } - - /// @dev 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)); - } - - /// @dev 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_); - } - - /// @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 return a pseudo-random claim - function _dummyClaim() internal view returns (Claim) { - return Claim.wrap(keccak256(abi.encode(gasleft()))); - } - - /// @dev 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)); - } -} - -contract FaultDispute_1v1_Actors_Test is FaultDisputeGame_Init { - /// @dev The honest actor - DisputeActor internal honest; - /// @dev The dishonest actor - DisputeActor internal dishonest; - - function setUp() public override { - // Setup the `FaultDisputeGame` - super.setUp(); - } - - /// @notice Fuzz test for a 1v1 output bisection dispute. - /// @dev 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 // - //////////////////////////////////////////////////////////////// - - /// @dev 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 - { - // 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)); - } - - /// @dev 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 }); - } - - /// @dev 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: gameProxy, - _l2Outputs: _honestL2Outputs, - _trace: _honestTrace, - _preStateData: _honestPreStateData - }); - dishonest = new HonestDisputeActor({ - _gameProxy: 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"); - } - - /// @dev 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, "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; - } - } - - /// @dev 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(); - } -} - -contract ClaimCreditReenter { - Vm internal immutable vm; - FaultDisputeGame internal immutable GAME; - uint256 public numCalls; - - constructor(FaultDisputeGame _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)); - } -} - -/// @dev 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)) - } -} From d131cbb816d66fe95b8f263e2f573b2684b97ace Mon Sep 17 00:00:00 2001 From: Zach Obront Date: Wed, 5 Jun 2024 14:44:51 -0700 Subject: [PATCH 07/13] IBlockDisputeGame interface abstraction --- .../src/dispute/OptimisticZKGame.sol | 30 ++++++++++++++----- .../dispute/interfaces/IBlockDisputeGame.sol | 22 ++++++++++++++ .../dispute/interfaces/IOptimisticZKGame.sol | 4 +-- 3 files changed, 47 insertions(+), 9 deletions(-) create mode 100644 packages/contracts-bedrock/src/dispute/interfaces/IBlockDisputeGame.sol diff --git a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol index ed53e19adc..6ed3248373 100644 --- a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol @@ -2,14 +2,13 @@ pragma solidity ^0.8.15; import { IDisputeGame } from "./interfaces/IDisputeGame.sol"; -import { IFaultDisputeGame } from "./interfaces/IFaultDisputeGame.sol"; +import { IBlockDisputeGame } from "./interfaces/IBlockDisputeGame.sol"; import { IDelayedWETH } from "./interfaces/IDelayedWETH.sol"; import { IInitializable } from "./interfaces/IInitializable.sol"; import { IOptimisticZKGame } from "./interfaces/IOptimisticZKGame.sol"; +import { IDisputeGameFactory } from "./interfaces/IDisputeGameFactory.sol"; -import { DisputeGameFactory } from "./DisputeGameFactory.sol"; -import { OutputRoot, GameStatus, Timestamp, Clock, Duration, GameType, Claim, Hash } from "src/dispute/lib/Types.sol"; -import { LibClock, LibDuration, LibTimestamp } from "./lib/LibUDT.sol"; +import "src/dispute/lib/Types.sol"; import { Clone } from "@solady/utils/Clone.sol"; import { SP1Verifier } from "@sp1-contracts/SP1Verifier.sol"; @@ -27,7 +26,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { uint constant GLOBAL_CHALLENGE_ID = type(uint64).max; /// @notice The DisputeGameFactory contract. - DisputeGameFactory immutable FACTORY; + IDisputeGameFactory immutable FACTORY; /// @notice The game type ID. GameType immutable GAME_TYPE; @@ -97,7 +96,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { if (_maxGameDuration.raw() != _maxProposerDuration.raw() * 2) revert InvalidDurations(); // Set all the immutable values in the implementation contract. - FACTORY = DisputeGameFactory(_factory); + FACTORY = IDisputeGameFactory(_factory); GAME_TYPE = _gameType; MAX_GAME_DURATION = _maxGameDuration; MAX_PROPOSER_DURATION = _maxProposerDuration; @@ -117,7 +116,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { // Pull the L2 block number from the previous game. // @audit this is a hack because it's not fault dispute game. why isn't this is core interface? - uint prevL2BlockNumber = IFaultDisputeGame(address(prevGame)).l2BlockNumber(); + uint prevL2BlockNumber = IBlockDisputeGame(address(prevGame)).l2BlockNumber(); // Set the anchorStateRoot to the previous game's root. anchorStateRoot = OutputRoot({ root: Hash.wrap(prevGame.rootClaim().raw()), l2BlockNumber: prevL2BlockNumber }); @@ -375,6 +374,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { // BONDS // //////////////////////////////////////////////////////////////// + // Deposit the bond into the DelayedWETH contract and increment the total bonds for the given challenge. function _depositBond(uint _challengeId) internal { if (msg.value != getRequiredBond(_challengeId)) revert WrongBondAmount(); if (_challengeId != GLOBAL_CHALLENGE_ID) challenges[_challengeId].totalBonds += msg.value; @@ -487,6 +487,22 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { proposerAddr_ = _getArgAddress(0x94); } + /// @notice Starting output root and block number of the game. + function startingOutputRoot() external view returns (Hash startingRoot_, uint256 l2BlockNumber_) { + startingRoot_ = Hash.wrap(anchorStateRoot.root.raw()); + l2BlockNumber_ = anchorStateRoot.l2BlockNumber; + } + + /// @notice Only the starting block number of the game. + function startingBlockNumber() external view returns (uint256 startingBlockNumber_) { + startingBlockNumber_ = anchorStateRoot.l2BlockNumber; + } + + /// @notice Only the starting output root of the game. + function startingRootHash() external view returns (Hash startingRootHash_) { + startingRootHash_ = Hash.wrap(anchorStateRoot.root.raw()); + } + /// @inheritdoc IDisputeGame function gameData() external view returns (GameType gameType_, Claim rootClaim_, bytes memory extraData_) { gameType_ = gameType(); diff --git a/packages/contracts-bedrock/src/dispute/interfaces/IBlockDisputeGame.sol b/packages/contracts-bedrock/src/dispute/interfaces/IBlockDisputeGame.sol new file mode 100644 index 0000000000..af82ac4f42 --- /dev/null +++ b/packages/contracts-bedrock/src/dispute/interfaces/IBlockDisputeGame.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IDisputeGame } from "./IDisputeGame.sol"; + +import "src/dispute/lib/Types.sol"; + +/// @title IBlockDisputeGame +/// @notice The interface for a game meant to resolve an L2 Block. +interface IBlockDisputeGame is IDisputeGame { + /// @notice The l2BlockNumber of the disputed output root in the `L2OutputOracle`. + function l2BlockNumber() external view returns (uint256 l2BlockNumber_); + + /// @notice Starting output root and block number of the game. + function startingOutputRoot() external view returns (Hash startingRoot_, uint256 l2BlockNumber_); + + /// @notice Only the starting block number of the game. + function startingBlockNumber() external view returns (uint256 startingBlockNumber_); + + /// @notice Only the starting output root of the game. + function startingRootHash() external view returns (Hash startingRootHash_); +} diff --git a/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol index 943eb9a880..1b8e96a256 100644 --- a/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; -import { IDisputeGame } from "./IDisputeGame.sol"; +import { IBlockDisputeGame } from "./IBlockDisputeGame.sol"; import "src/dispute/lib/Types.sol"; -interface IOptimisticZKGame is IDisputeGame { +interface IOptimisticZKGame is IBlockDisputeGame { enum IntermediateClaimStatus { NONE, ACCEPTED, From 8381ea7344d39ed44839d0fb3794564c960a14df Mon Sep 17 00:00:00 2001 From: Zach Obront Date: Wed, 5 Jun 2024 17:09:14 -0700 Subject: [PATCH 08/13] refactor public values --- .../src/dispute/OptimisticZKGame.sol | 62 +++++++++++-------- .../dispute/interfaces/IBlockDisputeGame.sol | 5 ++ .../dispute/interfaces/IOptimisticZKGame.sol | 8 +++ 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol index 6ed3248373..9d5190d55d 100644 --- a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol @@ -25,6 +25,12 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { /// @notice An ID used for the proposer's initial bond, since it doesn't fall into a specific challenge. uint constant GLOBAL_CHALLENGE_ID = type(uint64).max; + /// @notice The initial bond required to start a game. + uint constant INITIAL_BOND = 1 ether; + + /// @notice The bond required for each bisection. + uint constant BISECTION_BOND = 0.1 ether; + /// @notice The DisputeGameFactory contract. IDisputeGameFactory immutable FACTORY; @@ -115,7 +121,6 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { if (prevGame.status() != GameStatus.DEFENDER_WINS) revert PreviousGameNotResolved(); // Pull the L2 block number from the previous game. - // @audit this is a hack because it's not fault dispute game. why isn't this is core interface? uint prevL2BlockNumber = IBlockDisputeGame(address(prevGame)).l2BlockNumber(); // Set the anchorStateRoot to the previous game's root. @@ -254,7 +259,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { // RESOLUTION // //////////////////////////////////////////////////////////////// - function proveStep(uint _challengeId, bytes memory _proofBytes, bytes memory _blockTxData, bytes32 _alternateRoot) public payable { + function proveStep(uint _challengeId, bytes memory _proofBytes, PublicValuesStruct memory _publicValues) public payable { // Use the _challengeId to access the correct challenge struct. if (_challengeId >= challenges.length) revert InvalidChallengeId(); Challenge memory challenge = challenges[_challengeId]; @@ -270,17 +275,23 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { // If the right root has been challenged by the proposer, the challenger must prove that we can transition from left to right. if (challenge.right.status == IntermediateClaimStatus.CHALLENGED) { - // @todo how do we pull in the compressed tx data from blob? - bytes memory publicValues = abi.encode(challenge.left.outputRoot.root, challenge.right.outputRoot.root, _blockTxData); - verifyProof(VKEY, publicValues, _proofBytes); + if (challenge.left.outputRoot.root.raw() != _publicValues.l2PreRoot) revert InvalidRoot(); + if (challenge.right.outputRoot.root.raw() != _publicValues.l2PostRoot) revert InvalidRoot(); + // @todo verify relevant L1 block root matches _publicValues.l1Root + // @todo access correct kzg commitment to verify against _publicValues.blobKzgCommitment? + + verifyProof(VKEY, abi.encode(_publicValues), _proofBytes); // If the right root is accepted, it means nothing has been challenged. // The proposer is claiming that left (proposed block minus 1) transition to right (proposed block). // The challenger must prove that left (proposed block minus 1) transitions to something else. } else { - if (challenge.right.outputRoot.root.raw() == _alternateRoot) revert InvalidRoot(); - bytes memory publicValues = abi.encode(challenge.left.outputRoot.l2BlockNumber, _alternateRoot, _blockTxData); - verifyProof(VKEY, publicValues, _proofBytes); + if (challenge.left.outputRoot.root.raw() != _publicValues.l2PreRoot) revert InvalidRoot(); + if (challenge.right.outputRoot.root.raw() == _publicValues.l2PostRoot) revert InvalidRoot(); + // @todo verify relevant L1 block root matches _publicValues.l1Root + // @todo access correct kzg commitment to verify against _publicValues.blobKzgCommitment? + + verifyProof(VKEY, abi.encode(_publicValues), _proofBytes); } // Once the proof has been completed, resolve the game. @@ -291,8 +302,8 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { _resolveInternal(_challengeIds, msg.sender); } - /// @notice This function is called by the proposer to end the game if they were not successfully challenged. - // @audit this is a bit different from optimism functionality but matched it to match the interface + /// @notice This function is called to end the game in favor of the proposer if they were not successfully challenged. + /// @dev Unlike the Fraud Proof Game, this can't be called to settle to CHALLENGER_WINS, only DEFENDER_WINS. function resolve() public returns (GameStatus status_) { // We can only resolve an in progress game where the full game clock has run out. // If the proposer clock had run out or a ZK proof succeeded, the status would have been updated. @@ -438,10 +449,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { /// @dev It is important that the sum of totalBonds on a challenge is enough to justify the ZK work. /// Otherwise, the proposer could frontrun the proof to claim the larger bond back. function getRequiredBond(uint _challengeId) public pure returns (uint requiredBond_) { - // @todo think more about how to fairly calculate this so incentives are right - // keeping in mind that the proposer could frontrun all the disputes up to the proof? - // and that there might be games where there are no bisections - requiredBond_ = _challengeId == GLOBAL_CHALLENGE_ID ? 1 ether : 0.1 ether; + requiredBond_ = _challengeId == GLOBAL_CHALLENGE_ID ? INITIAL_BOND : BISECTION_BOND; } /// @inheritdoc IDisputeGame @@ -471,9 +479,11 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { extraData_ = _getArgBytes(0x54, 0x60); } - /// @return l2BlockNumber_ The block number that the game claiming is proving. - function l2BlockNumber() public pure returns (uint256 l2BlockNumber_) { - l2BlockNumber_ = _getArgUint256(0x54); + /// @inheritdoc IDisputeGame + function gameData() external view returns (GameType gameType_, Claim rootClaim_, bytes memory extraData_) { + gameType_ = gameType(); + rootClaim_ = rootClaim(); + extraData_ = extraData(); } @@ -487,26 +497,24 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { proposerAddr_ = _getArgAddress(0x94); } - /// @notice Starting output root and block number of the game. + /// @inheritdoc IBlockDisputeGame + function l2BlockNumber() public pure returns (uint256 l2BlockNumber_) { + l2BlockNumber_ = _getArgUint256(0x54); + } + + /// @inheritdoc IBlockDisputeGame function startingOutputRoot() external view returns (Hash startingRoot_, uint256 l2BlockNumber_) { startingRoot_ = Hash.wrap(anchorStateRoot.root.raw()); l2BlockNumber_ = anchorStateRoot.l2BlockNumber; } - /// @notice Only the starting block number of the game. + /// @inheritdoc IBlockDisputeGame function startingBlockNumber() external view returns (uint256 startingBlockNumber_) { startingBlockNumber_ = anchorStateRoot.l2BlockNumber; } - /// @notice Only the starting output root of the game. + /// @inheritdoc IBlockDisputeGame function startingRootHash() external view returns (Hash startingRootHash_) { startingRootHash_ = Hash.wrap(anchorStateRoot.root.raw()); } - - /// @inheritdoc IDisputeGame - function gameData() external view returns (GameType gameType_, Claim rootClaim_, bytes memory extraData_) { - gameType_ = gameType(); - rootClaim_ = rootClaim(); - extraData_ = extraData(); - } } diff --git a/packages/contracts-bedrock/src/dispute/interfaces/IBlockDisputeGame.sol b/packages/contracts-bedrock/src/dispute/interfaces/IBlockDisputeGame.sol index af82ac4f42..0965fcdae1 100644 --- a/packages/contracts-bedrock/src/dispute/interfaces/IBlockDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/interfaces/IBlockDisputeGame.sol @@ -9,14 +9,19 @@ import "src/dispute/lib/Types.sol"; /// @notice The interface for a game meant to resolve an L2 Block. interface IBlockDisputeGame is IDisputeGame { /// @notice The l2BlockNumber of the disputed output root in the `L2OutputOracle`. + /// @return l2BlockNumber_ The block number that the game claiming is proving. function l2BlockNumber() external view returns (uint256 l2BlockNumber_); /// @notice Starting output root and block number of the game. + /// @return startingRoot_ The root that the game claiming is proving. + /// @return l2BlockNumber_ The block number that the game claiming is proving. function startingOutputRoot() external view returns (Hash startingRoot_, uint256 l2BlockNumber_); /// @notice Only the starting block number of the game. + /// @return startingBlockNumber_ The block number that the game claiming is proving. function startingBlockNumber() external view returns (uint256 startingBlockNumber_); /// @notice Only the starting output root of the game. + /// @return startingRootHash_ The root that the game claiming is proving. function startingRootHash() external view returns (Hash startingRootHash_); } diff --git a/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol index 1b8e96a256..7b0ce22751 100644 --- a/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol @@ -26,6 +26,13 @@ interface IOptimisticZKGame is IBlockDisputeGame { bool resolved; } + struct PublicValuesStruct { + bytes32 l1Root; + bytes32 l2PreRoot; + bytes32 l2PostRoot; + bytes32 blobKzgCommitment; + } + error Unauthorized(); error ProposerIsChallenger(); error InvalidDurations(); @@ -42,4 +49,5 @@ interface IOptimisticZKGame is IBlockDisputeGame { error TransferFailed(); error ClockNotExpired(); error InvalidRoot(); + error InvalidBlobCommitment(); } From 7ed62d17b787bf625f32b47cdc7b5d5e6e5791d4 Mon Sep 17 00:00:00 2001 From: Zach Obront Date: Wed, 5 Jun 2024 17:17:26 -0700 Subject: [PATCH 09/13] pull general checks out of if blocks --- .../src/dispute/OptimisticZKGame.sol | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol index 9d5190d55d..44cf8bdfc5 100644 --- a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol @@ -273,24 +273,25 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { // Require that the left and right roots are adjacent and ready to prove. if (challenge.left.outputRoot.l2BlockNumber + 1 != challenge.right.outputRoot.l2BlockNumber) revert NotReadyToProve(); - // If the right root has been challenged by the proposer, the challenger must prove that we can transition from left to right. + // Validate public values passed to the verifier: The real left root of the game matches the passed l2PreRoot. + if (challenge.left.outputRoot.root.raw() != _publicValues.l2PreRoot) revert InvalidRoot(); + + // Validate public values passed to the verifier: The real L1 block root matches the passed l1Root. + // @todo verify relevant L1 block root matches _publicValues.l1Root + + // Validate public values passed to the verifier: The real commitment to the blob matches the passed blobKzgCommitment. + // @todo access correct kzg commitment to verify against _publicValues.blobKzgCommitment? + if (challenge.right.status == IntermediateClaimStatus.CHALLENGED) { - if (challenge.left.outputRoot.root.raw() != _publicValues.l2PreRoot) revert InvalidRoot(); + // If the right root has been challenged by the proposer, the challenger must prove that we CAN transition from left to right. + // Therefore, prove that the real right root matches the passed l2PostRoot. if (challenge.right.outputRoot.root.raw() != _publicValues.l2PostRoot) revert InvalidRoot(); - // @todo verify relevant L1 block root matches _publicValues.l1Root - // @todo access correct kzg commitment to verify against _publicValues.blobKzgCommitment? - verifyProof(VKEY, abi.encode(_publicValues), _proofBytes); - - // If the right root is accepted, it means nothing has been challenged. - // The proposer is claiming that left (proposed block minus 1) transition to right (proposed block). - // The challenger must prove that left (proposed block minus 1) transitions to something else. } else { - if (challenge.left.outputRoot.root.raw() != _publicValues.l2PreRoot) revert InvalidRoot(); + // If the right root is ACCEPTED, it means nothing has been challenged. + // The proposer is claiming that left (proposed block minus 1) DOES transition to right (proposed block). + // Therefore, the challenger must prove a block with an l2PostRoot that does NOT match the right root in the game. if (challenge.right.outputRoot.root.raw() == _publicValues.l2PostRoot) revert InvalidRoot(); - // @todo verify relevant L1 block root matches _publicValues.l1Root - // @todo access correct kzg commitment to verify against _publicValues.blobKzgCommitment? - verifyProof(VKEY, abi.encode(_publicValues), _proofBytes); } From 2d3f1b881d212742541fd92171d33811d98e88d4 Mon Sep 17 00:00:00 2001 From: Zach Obront Date: Wed, 5 Jun 2024 18:06:22 -0700 Subject: [PATCH 10/13] misc small fixes --- .../src/dispute/OptimisticZKGame.sol | 33 ++++++++++--------- .../dispute/interfaces/IBlockDisputeGame.sol | 14 -------- 2 files changed, 17 insertions(+), 30 deletions(-) diff --git a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol index 44cf8bdfc5..945e7c776d 100644 --- a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol @@ -88,6 +88,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { /// @param _weth The DelayedWETH contract. /// @param _originBlock The earliest block number that can be proved against. /// @param _maxBlockGap The largest allowed gap between the reference block and the block being proved. + /// @param _vkey The verification key used by the SP1Verifier contract. constructor( address _factory, GameType _gameType, @@ -117,7 +118,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { if (msg.sender != address(FACTORY)) revert Unauthorized(); // Query the factory to retrieve a game that has already been settled in favor of the defender. - (,, IDisputeGame prevGame) = FACTORY.gameAtIndex(startingRootIndex()); + (,, IDisputeGame prevGame) = FACTORY.gameAtIndex(startingRootGameIndex()); if (prevGame.status() != GameStatus.DEFENDER_WINS) revert PreviousGameNotResolved(); // Pull the L2 block number from the previous game. @@ -154,7 +155,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { // Don't allow proposer to challenge themselves. // This is needed so that we can trust that the proposer address winning means the root is valid. - if (msg.sender == proposer()) revert ProposerIsChallenger(); + if (msg.sender == gameCreator()) revert ProposerIsChallenger(); // Create a new challenge. challenges.push(Challenge({ @@ -219,7 +220,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { /// @param _accepted Whether the proposer accepts the proposed root or rejects it. function respondToSplit(uint _challengeId, bool _accepted) public payable { // Only allow the proposer to respond to splits. - if (msg.sender != proposer()) revert Unauthorized(); + if (msg.sender != gameCreator()) revert Unauthorized(); // Use the _challengeId to access the correct challenge struct. if (_challengeId >= challenges.length) revert InvalidChallengeId(); @@ -320,7 +321,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { cIds[challenges.length] = GLOBAL_CHALLENGE_ID; // Resolve all challenges in favor of the proposer, and update the game status. - _resolveInternal(cIds, proposer()); + _resolveInternal(cIds, gameCreator()); // Return the resulting game status. return status; @@ -377,7 +378,7 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { // If this is the first challenge being resolved, update the game status and resolvedAt timestamp. if (status == GameStatus.IN_PROGRESS) { - status = _recipient == proposer() ? GameStatus.DEFENDER_WINS : GameStatus.CHALLENGER_WINS; + status = _recipient == gameCreator() ? GameStatus.DEFENDER_WINS : GameStatus.CHALLENGER_WINS; resolvedAt = Timestamp.wrap(uint64(block.timestamp)); } } @@ -488,14 +489,9 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { } - /// @return startingRootIndex_ The index of the validated game we are proving from. - function startingRootIndex() public pure returns (uint256 startingRootIndex_) { - startingRootIndex_ = _getArgUint256(0x74); - } - - /// @return proposerAddr_ The address of the proposer for this game. - function proposer() public pure returns (address proposerAddr_) { - proposerAddr_ = _getArgAddress(0x94); + /// @return startingRootGameIndex_ The index of the validated game we are proving from. + function startingRootGameIndex() public pure returns (uint256 startingRootGameIndex_) { + startingRootGameIndex_ = _getArgUint256(0x74); } /// @inheritdoc IBlockDisputeGame @@ -503,18 +499,23 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { l2BlockNumber_ = _getArgUint256(0x54); } - /// @inheritdoc IBlockDisputeGame + + /// @notice Starting output root and block number of the game. + /// @return startingRoot_ The root that the game claiming is proving. + /// @return l2BlockNumber_ The block number that the game claiming is proving. function startingOutputRoot() external view returns (Hash startingRoot_, uint256 l2BlockNumber_) { startingRoot_ = Hash.wrap(anchorStateRoot.root.raw()); l2BlockNumber_ = anchorStateRoot.l2BlockNumber; } - /// @inheritdoc IBlockDisputeGame + /// @notice Only the starting block number of the game. + /// @return startingBlockNumber_ The block number that the game claiming is proving. function startingBlockNumber() external view returns (uint256 startingBlockNumber_) { startingBlockNumber_ = anchorStateRoot.l2BlockNumber; } - /// @inheritdoc IBlockDisputeGame + /// @notice Only the starting output root of the game. + /// @return startingRootHash_ The root that the game claiming is proving. function startingRootHash() external view returns (Hash startingRootHash_) { startingRootHash_ = Hash.wrap(anchorStateRoot.root.raw()); } diff --git a/packages/contracts-bedrock/src/dispute/interfaces/IBlockDisputeGame.sol b/packages/contracts-bedrock/src/dispute/interfaces/IBlockDisputeGame.sol index 0965fcdae1..d2a2fd5d80 100644 --- a/packages/contracts-bedrock/src/dispute/interfaces/IBlockDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/interfaces/IBlockDisputeGame.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; import { IDisputeGame } from "./IDisputeGame.sol"; - import "src/dispute/lib/Types.sol"; /// @title IBlockDisputeGame @@ -11,17 +10,4 @@ interface IBlockDisputeGame is IDisputeGame { /// @notice The l2BlockNumber of the disputed output root in the `L2OutputOracle`. /// @return l2BlockNumber_ The block number that the game claiming is proving. function l2BlockNumber() external view returns (uint256 l2BlockNumber_); - - /// @notice Starting output root and block number of the game. - /// @return startingRoot_ The root that the game claiming is proving. - /// @return l2BlockNumber_ The block number that the game claiming is proving. - function startingOutputRoot() external view returns (Hash startingRoot_, uint256 l2BlockNumber_); - - /// @notice Only the starting block number of the game. - /// @return startingBlockNumber_ The block number that the game claiming is proving. - function startingBlockNumber() external view returns (uint256 startingBlockNumber_); - - /// @notice Only the starting output root of the game. - /// @return startingRootHash_ The root that the game claiming is proving. - function startingRootHash() external view returns (Hash startingRootHash_); } From c08f1f53f2e4df529dec56692f8ebe3f14ce3704 Mon Sep 17 00:00:00 2001 From: Zach Obront Date: Thu, 27 Jun 2024 15:32:35 -0500 Subject: [PATCH 11/13] correct public inputs --- .../src/dispute/OptimisticZKGame.sol | 25 +++++++++++-------- .../dispute/interfaces/IOptimisticZKGame.sol | 8 +++--- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol index 945e7c776d..6257db25cf 100644 --- a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol @@ -171,7 +171,8 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { }), current: OutputRoot({ root: Hash.wrap(bytes32(0)), l2BlockNumber: 0 }), totalBonds: 0, - resolved: false + resolved: false, + l1Head: blockhash(block.number - 1) })); // Perform the first split on this new challenge. @@ -274,25 +275,27 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { // Require that the left and right roots are adjacent and ready to prove. if (challenge.left.outputRoot.l2BlockNumber + 1 != challenge.right.outputRoot.l2BlockNumber) revert NotReadyToProve(); - // Validate public values passed to the verifier: The real left root of the game matches the passed l2PreRoot. - if (challenge.left.outputRoot.root.raw() != _publicValues.l2PreRoot) revert InvalidRoot(); + // Validate public values passed to the verifier... - // Validate public values passed to the verifier: The real L1 block root matches the passed l1Root. - // @todo verify relevant L1 block root matches _publicValues.l1Root + // 1) The real left root of the game matches the passed l2PreRoot. + if (challenge.left.outputRoot.root.raw() != _publicValues.l2PreRoot) revert InvalidPublicInput(); - // Validate public values passed to the verifier: The real commitment to the blob matches the passed blobKzgCommitment. - // @todo access correct kzg commitment to verify against _publicValues.blobKzgCommitment? + // 2) The real L1 block hash at challenge time matches the passed L1 block hash. + if (challenge.l1Head != _publicValues.l1Head) revert InvalidPublicInput(); + + // 3) The real L2 block number being proven matches the passed L2 block number. + if (challenge.right.outputRoot.l2BlockNumber != _publicValues.l2BlockNumber) revert InvalidPublicInput(); if (challenge.right.status == IntermediateClaimStatus.CHALLENGED) { - // If the right root has been challenged by the proposer, the challenger must prove that we CAN transition from left to right. + // 4a) If the right root has been challenged by the proposer, the challenger must prove that we CAN transition from left to right. // Therefore, prove that the real right root matches the passed l2PostRoot. - if (challenge.right.outputRoot.root.raw() != _publicValues.l2PostRoot) revert InvalidRoot(); + if (challenge.right.outputRoot.root.raw() != _publicValues.l2PostRoot) revert InvalidPublicInput(); verifyProof(VKEY, abi.encode(_publicValues), _proofBytes); } else { - // If the right root is ACCEPTED, it means nothing has been challenged. + // 4b) If the right root is ACCEPTED, it means nothing has been challenged. // The proposer is claiming that left (proposed block minus 1) DOES transition to right (proposed block). // Therefore, the challenger must prove a block with an l2PostRoot that does NOT match the right root in the game. - if (challenge.right.outputRoot.root.raw() == _publicValues.l2PostRoot) revert InvalidRoot(); + if (challenge.right.outputRoot.root.raw() == _publicValues.l2PostRoot) revert InvalidPublicInput(); verifyProof(VKEY, abi.encode(_publicValues), _proofBytes); } diff --git a/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol index 7b0ce22751..c3b5c26dc3 100644 --- a/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol @@ -24,13 +24,14 @@ interface IOptimisticZKGame is IBlockDisputeGame { OutputRoot current; uint totalBonds; bool resolved; + bytes32 l1Head; } struct PublicValuesStruct { - bytes32 l1Root; + bytes32 l1Head; bytes32 l2PreRoot; bytes32 l2PostRoot; - bytes32 blobKzgCommitment; + bytes32 l2BlockNumber; } error Unauthorized(); @@ -48,6 +49,5 @@ interface IOptimisticZKGame is IBlockDisputeGame { error WrongBondAmount(); error TransferFailed(); error ClockNotExpired(); - error InvalidRoot(); - error InvalidBlobCommitment(); + error InvalidPublicInput(); } From 1d4b1bf61cc8187a6af4ccc3e247c06b62e24d06 Mon Sep 17 00:00:00 2001 From: Zach Obront Date: Fri, 28 Jun 2024 10:26:38 -0500 Subject: [PATCH 12/13] public l2 block num to uint --- packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol | 1 - .../src/dispute/interfaces/IOptimisticZKGame.sol | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol index 6257db25cf..6c9a8cff9d 100644 --- a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol @@ -276,7 +276,6 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { if (challenge.left.outputRoot.l2BlockNumber + 1 != challenge.right.outputRoot.l2BlockNumber) revert NotReadyToProve(); // Validate public values passed to the verifier... - // 1) The real left root of the game matches the passed l2PreRoot. if (challenge.left.outputRoot.root.raw() != _publicValues.l2PreRoot) revert InvalidPublicInput(); diff --git a/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol index c3b5c26dc3..8f57b94911 100644 --- a/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol @@ -31,7 +31,7 @@ interface IOptimisticZKGame is IBlockDisputeGame { bytes32 l1Head; bytes32 l2PreRoot; bytes32 l2PostRoot; - bytes32 l2BlockNumber; + uint256 l2BlockNumber; } error Unauthorized(); From edc066b681b9d5c79168aba003ec4bc861bc9c54 Mon Sep 17 00:00:00 2001 From: Zach Obront Date: Fri, 28 Jun 2024 10:50:52 -0500 Subject: [PATCH 13/13] add chain id to public values --- .../contracts-bedrock/src/dispute/OptimisticZKGame.sol | 7 +++++-- .../src/dispute/interfaces/IOptimisticZKGame.sol | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol index 6c9a8cff9d..11d7e21488 100644 --- a/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/OptimisticZKGame.sol @@ -285,13 +285,16 @@ contract OptimisticZKGame is IOptimisticZKGame, Clone, SP1Verifier { // 3) The real L2 block number being proven matches the passed L2 block number. if (challenge.right.outputRoot.l2BlockNumber != _publicValues.l2BlockNumber) revert InvalidPublicInput(); + // 4) The real chain ID matches the passed chain ID. + if (block.chainId != _publicValues.chainId) revert InvalidPublicInput(); + if (challenge.right.status == IntermediateClaimStatus.CHALLENGED) { - // 4a) If the right root has been challenged by the proposer, the challenger must prove that we CAN transition from left to right. + // 5a) If the right root has been challenged by the proposer, the challenger must prove that we CAN transition from left to right. // Therefore, prove that the real right root matches the passed l2PostRoot. if (challenge.right.outputRoot.root.raw() != _publicValues.l2PostRoot) revert InvalidPublicInput(); verifyProof(VKEY, abi.encode(_publicValues), _proofBytes); } else { - // 4b) If the right root is ACCEPTED, it means nothing has been challenged. + // 5b) If the right root is ACCEPTED, it means nothing has been challenged. // The proposer is claiming that left (proposed block minus 1) DOES transition to right (proposed block). // Therefore, the challenger must prove a block with an l2PostRoot that does NOT match the right root in the game. if (challenge.right.outputRoot.root.raw() == _publicValues.l2PostRoot) revert InvalidPublicInput(); diff --git a/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol b/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol index 8f57b94911..7344137821 100644 --- a/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol +++ b/packages/contracts-bedrock/src/dispute/interfaces/IOptimisticZKGame.sol @@ -32,6 +32,7 @@ interface IOptimisticZKGame is IBlockDisputeGame { bytes32 l2PreRoot; bytes32 l2PostRoot; uint256 l2BlockNumber; + uint256 chainId; } error Unauthorized();