// SPDX-License-Identifier: MIT pragma solidity 0.8.15; import "./interfaces/ILockingPool.sol"; import "contracts/L1/dispute/lib/Errors.sol"; import "contracts/L1/dispute/lib/Types.sol"; import {Clone} from "solady/src/utils/Clone.sol"; import {FixedPointMathLib} from "solady/src/utils/FixedPointMathLib.sol"; import {Hashing} from "../../libraries/Hashing.sol"; import {IBigStepper} from "./interfaces/IBigStepper.sol"; import {IDelayedWETH} from "./interfaces/IDelayedWETH.sol"; import {IDisputeGame} from "./interfaces/IDisputeGame.sol"; import {IFaultDisputeGame} from "./interfaces/IFaultDisputeGame.sol"; import {IInitializable} from "contracts/L1/dispute/interfaces/IInitializable.sol"; import {IMVMStateCommitmentChain} from "../rollup/IMVMStateCommitmentChain.sol"; import {IPreimageOracle} from "../cannon/interfaces/IPreimageOracle.sol"; import {ISemver} from "../../universal/ISemver.sol"; import {Lib_AddressManager} from "../../libraries/resolver/Lib_AddressManager.sol"; import {Lib_OVMCodec} from "../../libraries/codec/Lib_OVMCodec.sol"; import {Lib_RLPReader} from "../../libraries/rlp/Lib_RLPReader.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {OutputRoot, LocalPreimageKey, VMStatuses} from "./lib/Types.sol"; import {StateCommitmentChain} from "../rollup/StateCommitmentChain.sol"; import {Types} from "../../libraries/Types.sol"; /// @title FaultDisputeGame /// @notice An implementation of the `IFaultDisputeGame` interface. contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { //////////////////////////////////////////////////////////////// // State Vars // //////////////////////////////////////////////////////////////// /// @notice The absolute prestate of the instruction trace. This is a constant that is defined /// by the program that is being used to execute the trace. Claim internal immutable ABSOLUTE_PRESTATE; /// @notice The max depth of the game. uint256 internal immutable MAX_GAME_DEPTH; /// @notice The max depth of the output bisection portion of the position tree. Immediately beneath /// this depth, execution trace bisection begins. uint256 internal immutable SPLIT_DEPTH; /// @notice The maximum duration that may accumulate on a team's chess clock before they may no longer respond. Duration internal immutable MAX_CLOCK_DURATION; /// @notice An onchain VM that performs single instruction steps on a fault proof program trace. IBigStepper internal immutable VM; /// @notice The game type ID. GameType internal immutable GAME_TYPE; /// @notice WETH contract for holding ETH. IDelayedWETH internal immutable WETH; /// @notice The address manager contract. Lib_AddressManager internal immutable ADDRESS_MANAGER; /// @notice The chain ID of the L2 network this contract argues about. uint256 internal immutable L2_CHAIN_ID; /// @notice The duration of the clock extension. Will be doubled if the grandchild is the root claim of an execution /// trace bisection subgame. Duration internal immutable CLOCK_EXTENSION; /// @notice The global root claim's position is always at gindex 1. Position internal constant ROOT_POSITION = Position.wrap(1); /// @notice The index of the block number in the RLP-encoded block header. /// @dev Consensus encoding reference: /// https://github.com/paradigmxyz/reth/blob/5f82993c23164ce8ccdc7bf3ae5085205383a5c8/crates/primitives/src/header.rs#L368 uint256 internal constant HEADER_BLOCK_NUMBER_INDEX = 8; /// @notice Semantic version. /// @custom:semver 1.3.0-rc.1 string public constant version = "1.3.0-rc.1"; /// @notice The starting timestamp of the game Timestamp public createdAt; /// @notice The timestamp of the game's global resolution. Timestamp public resolvedAt; /// @inheritdoc IDisputeGame GameStatus public status; /// @notice Flag for the `initialize` function to prevent re-initialization. bool internal initialized; /// @notice Flag for whether or not the L2 block number claim has been invalidated via `challengeRootL2Block`. bool public l2BlockNumberChallenged; /// @notice The challenger of the L2 block number claim. Should always be `address(0)` if `l2BlockNumberChallenged` /// is `false`. Should be the address of the challenger if `l2BlockNumberChallenged` is `true`. address public l2BlockNumberChallenger; /// @notice An append-only array of all claims made during the dispute game. ClaimData[] public claimData; /// @notice Credited balances for winning participants. mapping(address => uint256) public credit; /// @notice A mapping to allow for constant-time lookups of existing claims. mapping(Hash => bool) public claims; /// @notice A mapping of subgames rooted at a claim index to other claim indices in the subgame. mapping(uint256 => uint256[]) public subgames; /// @notice A mapping of resolved subgames rooted at a claim index. mapping(uint256 => bool) public resolvedSubgames; /// @notice A mapping of claim indices to resolution checkpoints. mapping(uint256 => ResolutionCheckpoint) public resolutionCheckpoints; /// @notice The latest finalized output root, serving as the anchor for output bisection. OutputRoot public startingOutputRoot; /// @notice The name of the SCC contract. string internal constant SCC_NAME = "StateCommitmentChain"; /// @notice The name of the locking pool contract. string internal constant LOCKING_POOL_NAME = "FaultProofLockingPool"; /// @param _gameType The type ID of the game. /// @param _absolutePrestate The absolute prestate of the instruction trace. /// @param _maxGameDepth The maximum depth of bisection. /// @param _splitDepth The final depth of the output bisection portion of the game. /// @param _clockExtension The clock extension to perform when the remaining duration is less than the extension. /// @param _maxClockDuration The maximum amount of time that may accumulate on a team's chess clock. /// @param _vm An onchain VM that performs single instruction steps on an FPP trace. /// @param _weth WETH contract for holding ETH. /// @param _addressManager The contract that stores the names and addresses of metis contracts. /// @param _l2ChainId Chain ID of the L2 network this contract argues about. constructor( GameType _gameType, Claim _absolutePrestate, uint256 _maxGameDepth, uint256 _splitDepth, Duration _clockExtension, Duration _maxClockDuration, IBigStepper _vm, IDelayedWETH _weth, Lib_AddressManager _addressManager, uint256 _l2ChainId ) { // The max game depth may not be greater than `LibPosition.MAX_POSITION_BITLEN - 1`. if (_maxGameDepth > LibPosition.MAX_POSITION_BITLEN - 1) revert MaxDepthTooLarge(); // The split depth plus one cannot be greater than or equal to the max game depth. We add // an additional depth to the split depth to avoid a bug in trace ancestor lookup. We know // that the case where the split depth is the max value for uint256 is equivalent to the // second check though we do need to check it explicitly to avoid an overflow. if (_splitDepth == type(uint256).max || _splitDepth + 1 >= _maxGameDepth) revert InvalidSplitDepth(); // The split depth cannot be 0 or 1 to stay in bounds of clock extension arithmetic. if (_splitDepth < 2) revert InvalidSplitDepth(); // The PreimageOracle challenge period must fit into uint64 so we can safely use it here. // Runtime check was added instead of changing the ABI since the contract is already // deployed in production. We perform the same check within the PreimageOracle for the // benefit of developers but also perform this check here defensively. if (_vm.oracle().challengePeriod() > type(uint64).max) revert InvalidChallengePeriod(); // Determine the maximum clock extension which is either the split depth extension or the // maximum game depth extension depending on the configuration of these contracts. uint256 splitDepthExtension = uint256(_clockExtension.raw()) * 2; uint256 maxGameDepthExtension = uint256(_clockExtension.raw()) + uint256(_vm.oracle().challengePeriod()); uint256 maxClockExtension = Math.max(splitDepthExtension, maxGameDepthExtension); // The maximum clock extension must fit into a uint64. if (maxClockExtension > type(uint64).max) revert InvalidClockExtension(); // The maximum clock extension may not be greater than the maximum clock duration. if (uint64(maxClockExtension) > _maxClockDuration.raw()) revert InvalidClockExtension(); // Set up initial game state. GAME_TYPE = _gameType; ABSOLUTE_PRESTATE = _absolutePrestate; MAX_GAME_DEPTH = _maxGameDepth; SPLIT_DEPTH = _splitDepth; CLOCK_EXTENSION = _clockExtension; MAX_CLOCK_DURATION = _maxClockDuration; VM = _vm; WETH = _weth; ADDRESS_MANAGER = _addressManager; L2_CHAIN_ID = _l2ChainId; } /// @inheritdoc IInitializable function initialize() public payable virtual { // SAFETY: Any revert in this function will bubble up to the DisputeGameFactory and // prevent the game from being created. // // Implicit assumptions: // - The `gameStatus` state variable defaults to 0, which is `GameStatus.IN_PROGRESS` // - The dispute game factory will enforce the required bond to initialize the game. // // Explicit checks: // - The game must not have already been initialized. // - An output root cannot be proposed at or before the starting block number. // INVARIANT: The game must not have already been initialized. if (initialized) revert AlreadyInitialized(); // Grab the latest anchor root. IMVMStateCommitmentChain stateCommitmentChain = IMVMStateCommitmentChain(ADDRESS_MANAGER.getAddress(SCC_NAME)); (bytes32 scRoot, uint256 rootBlockNumber) = stateCommitmentChain.findEarliestDisputableBatch(L2_CHAIN_ID); Hash root = Hash.wrap(scRoot); // Should only happen if this is a new game type that hasn't been set up yet. if (root.raw() == bytes32(0)) revert AnchorRootNotFound(); // we do not allow to dispute an already disputed batch if (stateCommitmentChain.isDisputedBatch(scRoot)) revert ClaimAlreadyResolved(); // Set the starting output root. startingOutputRoot = OutputRoot({l2BlockNumber: rootBlockNumber, root: root}); // Revert if the calldata size is not the expected length. // // This is to prevent adding extra or omitting bytes from to `extraData` that result in a different game UUID // in the factory, but are not used by the game, which would allow for multiple dispute games for the same // output proposal to be created. // // Expected length: 0x7A // - 0x04 selector // - 0x14 creator address // - 0x20 root claim // - 0x20 l1 head // - 0x20 extraData // - 0x02 CWIA bytes assembly { if iszero(eq(calldatasize(), 0x7A)) { // Store the selector for `BadExtraData()` & revert mstore(0x00, 0x9824bdab) revert(0x1C, 0x04) } } // Do not allow the game to be initialized if the root claim corresponds to a block at or before the // configured starting block number. if (l2BlockNumber() <= rootBlockNumber) revert UnexpectedRootClaim(rootClaim()); // Set the root claim claimData.push( ClaimData({ parentIndex: type(uint32).max, counteredBy: address(0), claimant: gameCreator(), bond: uint128(msg.value), claim: rootClaim(), position: ROOT_POSITION, clock: LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))) }) ); // Set the game as initialized. initialized = true; // Deposit the bond. WETH.deposit{value: msg.value}(); // Set the game's starting timestamp createdAt = Timestamp.wrap(uint64(block.timestamp)); } //////////////////////////////////////////////////////////////// // `IFaultDisputeGame` impl // //////////////////////////////////////////////////////////////// /// @inheritdoc IFaultDisputeGame function step( uint256 _claimIndex, bool _isAttack, bytes calldata _stateData, bytes calldata _proof ) public virtual { // INVARIANT: Steps cannot be made unless the game is currently in progress. if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); // Get the parent. If it does not exist, the call will revert with OOB. ClaimData storage parent = claimData[_claimIndex]; // Pull the parent position out of storage. Position parentPos = parent.position; // Determine the position of the step. Position stepPos = parentPos.move(_isAttack); // INVARIANT: A step cannot be made unless the move position is 1 below the `MAX_GAME_DEPTH` if (stepPos.depth() != MAX_GAME_DEPTH + 1) revert InvalidParent(); // Determine the expected pre & post states of the step. Claim preStateClaim; ClaimData storage postState; if (_isAttack) { // If the step position's index at depth is 0, the prestate is the absolute // prestate. // If the step is an attack at a trace index > 0, the prestate exists elsewhere in // the game state. // NOTE: We localize the `indexAtDepth` for the current execution trace subgame by finding // the remainder of the index at depth divided by 2 ** (MAX_GAME_DEPTH - SPLIT_DEPTH), // which is the number of leaves in each execution trace subgame. This is so that we can // determine whether or not the step position is represents the `ABSOLUTE_PRESTATE`. preStateClaim = (stepPos.indexAtDepth() % (1 << (MAX_GAME_DEPTH - SPLIT_DEPTH))) == 0 ? ABSOLUTE_PRESTATE : _findTraceAncestor(Position.wrap(parentPos.raw() - 1), parent.parentIndex, false).claim; // For all attacks, the poststate is the parent claim. postState = parent; } else { // If the step is a defense, the poststate exists elsewhere in the game state, // and the parent claim is the expected pre-state. preStateClaim = parent.claim; postState = _findTraceAncestor(Position.wrap(parentPos.raw() + 1), parent.parentIndex, false); } // INVARIANT: The prestate is always invalid if the passed `_stateData` is not the // preimage of the prestate claim hash. // We ignore the highest order byte of the digest because it is used to // indicate the VM Status and is added after the digest is computed. if (keccak256(_stateData) << 8 != preStateClaim.raw() << 8) revert InvalidPrestate(); // Compute the local preimage context for the step. Hash uuid = _findLocalContext(_claimIndex); // INVARIANT: If a step is an attack, the poststate is valid if the step produces // the same poststate hash as the parent claim's value. // If a step is a defense: // 1. If the parent claim and the found post state agree with each other // (depth diff % 2 == 0), the step is valid if it produces the same // state hash as the post state's claim. // 2. If the parent claim and the found post state disagree with each other // (depth diff % 2 != 0), the parent cannot be countered unless the step // produces the same state hash as `postState.claim`. // SAFETY: While the `attack` path does not need an extra check for the post // state's depth in relation to the parent, we don't need another // branch because (n - n) % 2 == 0. bool validStep = VM.step(_stateData, _proof, uuid.raw()) == postState.claim.raw(); bool parentPostAgree = (parentPos.depth() - postState.position.depth()) % 2 == 0; if (parentPostAgree == validStep) revert ValidStep(); // INVARIANT: A step cannot be made against a claim for a second time. if (parent.counteredBy != address(0)) revert DuplicateStep(); // Set the parent claim as countered. We do not need to append a new claim to the game; // instead, we can just set the existing parent as countered. parent.counteredBy = msg.sender; } /// @notice Generic move function, used for both `attack` and `defend` moves. /// @param _disputed The disputed `Claim`. /// @param _challengeIndex The index of the claim being moved against. /// @param _claim The claim at the next logical position in the game. /// @param _isAttack Whether or not the move is an attack or defense. function move(Claim _disputed, uint256 _challengeIndex, Claim _claim, bool _isAttack) public payable virtual { // INVARIANT: Moves cannot be made unless the game is currently in progress. if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); // Get the parent. If it does not exist, the call will revert with OOB. ClaimData memory parent = claimData[_challengeIndex]; // INVARIANT: The claim at the _challengeIndex must be the disputed claim. if (Claim.unwrap(parent.claim) != Claim.unwrap(_disputed)) revert InvalidDisputedClaimIndex(); // Compute the position that the claim commits to. Because the parent's position is already // known, we can compute the next position by moving left or right depending on whether // or not the move is an attack or defense. Position parentPos = parent.position; Position nextPosition = parentPos.move(_isAttack); uint256 nextPositionDepth = nextPosition.depth(); // INVARIANT: A defense can never be made against the root claim of either the output root game or any // of the execution trace bisection subgames. This is because the root claim commits to the // entire state. Therefore, the only valid defense is to do nothing if it is agreed with. if ((_challengeIndex == 0 || nextPositionDepth == SPLIT_DEPTH + 2) && !_isAttack) { revert CannotDefendRootClaim(); } // INVARIANT: No moves against the root claim can be made after it has been challenged with // `challengeRootL2Block`.` if (l2BlockNumberChallenged && _challengeIndex == 0) revert L2BlockNumberChallenged(); // INVARIANT: A move can never surpass the `MAX_GAME_DEPTH`. The only option to counter a // claim at this depth is to perform a single instruction step on-chain via // the `step` function to prove that the state transition produces an unexpected // post-state. if (nextPositionDepth > MAX_GAME_DEPTH) revert GameDepthExceeded(); // When the next position surpasses the split depth (i.e., it is the root claim of an execution // trace bisection sub-game), we need to perform some extra verification steps. if (nextPositionDepth == SPLIT_DEPTH + 1) { _verifyExecBisectionRoot(_claim, _challengeIndex, parentPos, _isAttack); } // INVARIANT: The `msg.value` must exactly equal the required bond. if (getRequiredBond(nextPosition) != msg.value) revert IncorrectBondAmount(); // Compute the duration of the next clock. This is done by adding the duration of the // grandparent claim to the difference between the current block timestamp and the // parent's clock timestamp. Duration nextDuration = getChallengerDuration(_challengeIndex); // INVARIANT: A move can never be made once its clock has exceeded `MAX_CLOCK_DURATION` // seconds of time. if (nextDuration.raw() == MAX_CLOCK_DURATION.raw()) revert ClockTimeExceeded(); // Clock extension is a mechanism that automatically extends the clock for a potential // grandchild claim when there would be less than the clock extension time left if a player // is forced to inherit another team's clock when countering a freeloader claim. Exact // amount of clock extension time depends exactly where we are within the game. uint64 actualExtension; if (nextPositionDepth == MAX_GAME_DEPTH - 1) { // If the next position is `MAX_GAME_DEPTH - 1` then we're about to execute a step. Our // clock extension must therefore account for the LPP challenge period in addition to // the standard clock extension. actualExtension = CLOCK_EXTENSION.raw() + uint64(VM.oracle().challengePeriod()); } else if (nextPositionDepth == SPLIT_DEPTH - 1) { // If the next position is `SPLIT_DEPTH - 1` then we're about to begin an execution // trace bisection and we need to give extra time for the off-chain challenge agent to // be able to generate the initial instruction trace on the native FPVM. actualExtension = CLOCK_EXTENSION.raw() * 2; } else { // Otherwise, we just use the standard clock extension. actualExtension = CLOCK_EXTENSION.raw(); } // Check if we need to apply the clock extension. if (nextDuration.raw() > MAX_CLOCK_DURATION.raw() - actualExtension) { nextDuration = Duration.wrap(MAX_CLOCK_DURATION.raw() - actualExtension); } // Construct the next clock with the new duration and the current block timestamp. Clock nextClock = LibClock.wrap(nextDuration, Timestamp.wrap(uint64(block.timestamp))); // INVARIANT: There cannot be multiple identical claims with identical moves on the same challengeIndex. Multiple // claims at the same position may dispute the same challengeIndex. However, they must have different // values. Hash claimHash = _claim.hashClaimPos(nextPosition, _challengeIndex); if (claims[claimHash]) revert ClaimAlreadyExists(); claims[claimHash] = true; // Create the new claim. claimData.push( ClaimData({ parentIndex: uint32(_challengeIndex), // This is updated during subgame resolution counteredBy: address(0), claimant: msg.sender, bond: uint128(msg.value), claim: _claim, position: nextPosition, clock: nextClock }) ); // Update the subgame rooted at the parent claim. subgames[_challengeIndex].push(claimData.length - 1); // Deposit the bond. WETH.deposit{value: msg.value}(); // Emit the appropriate event for the attack or defense. emit Move(_challengeIndex, _claim, msg.sender); } /// @inheritdoc IFaultDisputeGame function attack(Claim _disputed, uint256 _parentIndex, Claim _claim) external payable { move(_disputed, _parentIndex, _claim, true); } /// @inheritdoc IFaultDisputeGame function defend(Claim _disputed, uint256 _parentIndex, Claim _claim) external payable { move(_disputed, _parentIndex, _claim, false); } /// @inheritdoc IFaultDisputeGame function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external { // INVARIANT: Local data can only be added if the game is currently in progress. if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); (Claim starting, Position startingPos, Claim disputed, Position disputedPos) = _findStartingAndDisputedOutputs(_execLeafIdx); Hash uuid = _computeLocalContext(starting, startingPos, disputed, disputedPos); IPreimageOracle oracle = VM.oracle(); if (_ident == LocalPreimageKey.L1_HEAD_HASH) { // Load the L1 head hash oracle.loadLocalData(_ident, uuid.raw(), l1Head().raw(), 32, _partOffset); } else if (_ident == LocalPreimageKey.STARTING_OUTPUT_ROOT) { // Load the starting proposal's output root. oracle.loadLocalData(_ident, uuid.raw(), starting.raw(), 32, _partOffset); } else if (_ident == LocalPreimageKey.DISPUTED_OUTPUT_ROOT) { // Load the disputed proposal's output root oracle.loadLocalData(_ident, uuid.raw(), disputed.raw(), 32, _partOffset); } else if (_ident == LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER) { // Load the disputed proposal's L2 block number as a big-endian uint64 in the // high order 8 bytes of the word. // We add the index at depth + 1 to the starting block number to get the disputed L2 // block number. uint256 l2Number = startingOutputRoot.l2BlockNumber + disputedPos.traceIndex(SPLIT_DEPTH) + 1; // Choose the minimum between the `l2BlockNumber` claim and the bisected-to L2 block number. l2Number = l2Number < l2BlockNumber() ? l2Number : l2BlockNumber(); oracle.loadLocalData(_ident, uuid.raw(), bytes32(l2Number << 0xC0), 8, _partOffset); } else if (_ident == LocalPreimageKey.CHAIN_ID) { // Load the chain ID as a big-endian uint64 in the high order 8 bytes of the word. oracle.loadLocalData(_ident, uuid.raw(), bytes32(L2_CHAIN_ID << 0xC0), 8, _partOffset); } else { revert InvalidLocalIdent(); } } /// @inheritdoc IFaultDisputeGame function getNumToResolve(uint256 _claimIndex) public view returns (uint256 numRemainingChildren_) { ResolutionCheckpoint storage checkpoint = resolutionCheckpoints[_claimIndex]; uint256[] storage challengeIndices = subgames[_claimIndex]; uint256 challengeIndicesLen = challengeIndices.length; numRemainingChildren_ = challengeIndicesLen - checkpoint.subgameIndex; } /// @inheritdoc IFaultDisputeGame function l2BlockNumber() public pure returns (uint256 l2BlockNumber_) { l2BlockNumber_ = _getArgUint256(0x54); } /// @inheritdoc IFaultDisputeGame function startingBlockNumber() external view returns (uint256 startingBlockNumber_) { startingBlockNumber_ = startingOutputRoot.l2BlockNumber; } /// @inheritdoc IFaultDisputeGame function startingRootHash() external view returns (Hash startingRootHash_) { startingRootHash_ = startingOutputRoot.root; } /// @notice Challenges the root L2 block number by providing the preimage of the output root and the L2 block header /// and showing that the committed L2 block number is incorrect relative to the claimed L2 block number. /// @param _outputRootProof The output root proof. /// @param _headerRLP The RLP-encoded L2 block header. function challengeRootL2Block( Lib_OVMCodec.ChainBatchHeader calldata _outputRootProof, bytes calldata _headerRLP ) external { // INVARIANT: Moves cannot be made unless the game is currently in progress. if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); // The root L2 block claim can only be challenged once. if (l2BlockNumberChallenged) revert L2BlockNumberChallenged(); // Verify the output root preimage. if (Lib_OVMCodec.hashBatchHeader(_outputRootProof) != rootClaim().raw()) revert InvalidOutputRootProof(); // Verify the block hash preimage. (, , bytes32 latestBlockhash, ) = abi.decode(_outputRootProof.extraData, (uint256, address, bytes32, uint256)); if (keccak256(_headerRLP) != latestBlockhash) revert InvalidHeaderRLP(); // Decode the header RLP to find the number of the block. In the consensus encoding, the timestamp // is the 9th element in the list that represents the block header. Lib_RLPReader.RLPItem[] memory headerContents = Lib_RLPReader.readList(Lib_RLPReader.toRLPItem(_headerRLP)); bytes memory rawBlockNumber = Lib_RLPReader.readBytes(headerContents[HEADER_BLOCK_NUMBER_INDEX]); // Sanity check the block number string length. if (rawBlockNumber.length > 32) revert InvalidHeaderRLP(); // Convert the raw, left-aligned block number to a uint256 by aligning it as a big-endian // number in the low-order bytes of a 32-byte word. // // SAFETY: The length of `rawBlockNumber` is checked above to ensure it is at most 32 bytes. uint256 blockNumber; assembly { blockNumber := shr(shl(0x03, sub(0x20, mload(rawBlockNumber))), mload(add(rawBlockNumber, 0x20))) } // Ensure the block number does not match the block number claimed in the dispute game. if (blockNumber == l2BlockNumber()) revert BlockNumberMatches(); // check block number is in batch range if (blockNumber < _outputRootProof.prevTotalElements + 1 || blockNumber > _outputRootProof.prevTotalElements + _outputRootProof.batchSize) { revert InvalidHeaderRLP(); } // Issue a special counter to the root claim. This counter will always win the root claim subgame, and receive // the bond from the root claimant. l2BlockNumberChallenger = msg.sender; l2BlockNumberChallenged = true; } //////////////////////////////////////////////////////////////// // `IDisputeGame` impl // //////////////////////////////////////////////////////////////// /// @inheritdoc IDisputeGame function resolve() external returns (GameStatus status_) { // INVARIANT: Resolution cannot occur unless the game is currently in progress. if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); // INVARIANT: Resolution cannot occur unless the absolute root subgame has been resolved. if (!resolvedSubgames[0]) revert OutOfOrderResolution(); // Update the global game status; The dispute has concluded. status_ = claimData[0].counteredBy == address(0) ? GameStatus.DEFENDER_WINS : GameStatus.CHALLENGER_WINS; resolvedAt = Timestamp.wrap(uint64(block.timestamp)); // Update the status and emit the resolved event, note that we're performing an assignment here. emit Resolved(status = status_); // Handle locking pool slashing if challenger wins if (status_ == GameStatus.CHALLENGER_WINS) { address lockingPool = ADDRESS_MANAGER.getAddress(LOCKING_POOL_NAME); if (lockingPool != address(0)) { // Get the winning claimant (the one who successfully countered the root claim) address winner = claimData[0].counteredBy; // Call slash on the locking pool try ILockingPool(lockingPool).slash(winner) { // Slashing succeeded } catch (bytes memory reason) { emit SlashingFailed(winner, reason); } } // Mark batch as disputed IMVMStateCommitmentChain(ADDRESS_MANAGER.getAddress(SCC_NAME)).saveDisputedBatch(claimData[0].claim.raw()); } } /// @inheritdoc IFaultDisputeGame function resolveClaim(uint256 _claimIndex, uint256 _numToResolve) external { // INVARIANT: Resolution cannot occur unless the game is currently in progress. if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); ClaimData storage subgameRootClaim = claimData[_claimIndex]; Duration challengeClockDuration = getChallengerDuration(_claimIndex); // INVARIANT: Cannot resolve a subgame unless the clock of its would-be counter has expired // INVARIANT: Assuming ordered subgame resolution, challengeClockDuration is always >= MAX_CLOCK_DURATION if all // descendant subgames are resolved if (challengeClockDuration.raw() < MAX_CLOCK_DURATION.raw()) revert ClockNotExpired(); // INVARIANT: Cannot resolve a subgame twice. if (resolvedSubgames[_claimIndex]) revert ClaimAlreadyResolved(); uint256[] storage challengeIndices = subgames[_claimIndex]; uint256 challengeIndicesLen = challengeIndices.length; // Uncontested claims are resolved implicitly unless they are the root claim. Pay out the bond to the claimant // and return early. if (challengeIndicesLen == 0 && _claimIndex != 0) { // In the event that the parent claim is at the max depth, there will always be 0 subgames. If the // `counteredBy` field is set and there are no subgames, this implies that the parent claim was successfully // stepped against. In this case, we pay out the bond to the party that stepped against the parent claim. // Otherwise, the parent claim is uncontested, and the bond is returned to the claimant. address counteredBy = subgameRootClaim.counteredBy; address recipient = counteredBy == address(0) ? subgameRootClaim.claimant : counteredBy; _distributeBond(recipient, subgameRootClaim); resolvedSubgames[_claimIndex] = true; return; } // Fetch the resolution checkpoint from storage. ResolutionCheckpoint memory checkpoint = resolutionCheckpoints[_claimIndex]; // If the checkpoint does not currently exist, initialize the current left most position as max u128. if (!checkpoint.initialCheckpointComplete) { checkpoint.leftmostPosition = Position.wrap(type(uint128).max); checkpoint.initialCheckpointComplete = true; // If `_numToResolve == 0`, assume that we can check all child subgames in this one callframe. if (_numToResolve == 0) _numToResolve = challengeIndicesLen; } // Assume parent is honest until proven otherwise uint256 lastToResolve = checkpoint.subgameIndex + _numToResolve; uint256 finalCursor = lastToResolve > challengeIndicesLen ? challengeIndicesLen : lastToResolve; for (uint256 i = checkpoint.subgameIndex; i < finalCursor; i++) { uint256 challengeIndex = challengeIndices[i]; // INVARIANT: Cannot resolve a subgame containing an unresolved claim if (!resolvedSubgames[challengeIndex]) revert OutOfOrderResolution(); ClaimData storage claim = claimData[challengeIndex]; // If the child subgame is uncountered and further left than the current left-most counter, // update the parent subgame's `countered` address and the current `leftmostCounter`. // The left-most correct counter is preferred in bond payouts in order to discourage attackers // from countering invalid subgame roots via an invalid defense position. As such positions // cannot be correctly countered. // Note that correctly positioned defense, but invalid claimes can still be successfully countered. if (claim.counteredBy == address(0) && checkpoint.leftmostPosition.raw() > claim.position.raw()) { checkpoint.counteredBy = claim.claimant; checkpoint.leftmostPosition = claim.position; } } // Increase the checkpoint's cursor position by the number of children that were checked. checkpoint.subgameIndex = uint32(finalCursor); // Persist the checkpoint and allow for continuing in a separate transaction, if resolution is not already // complete. resolutionCheckpoints[_claimIndex] = checkpoint; // If all children have been traversed in the above loop, the subgame may be resolved. Otherwise, persist the // checkpoint and allow for continuation in a separate transaction. if (checkpoint.subgameIndex == challengeIndicesLen) { address countered = checkpoint.counteredBy; // Mark the subgame as resolved. resolvedSubgames[_claimIndex] = true; // Distribute the bond to the appropriate party. if (_claimIndex == 0 && l2BlockNumberChallenged) { // Special case: If the root claim has been challenged with the `challengeRootL2Block` function, // the bond is always paid out to the issuer of that challenge. address challenger = l2BlockNumberChallenger; _distributeBond(challenger, subgameRootClaim); subgameRootClaim.counteredBy = challenger; } else { // If the parent was not successfully countered, pay out the parent's bond to the claimant. // If the parent was successfully countered, pay out the parent's bond to the challenger. _distributeBond(countered == address(0) ? subgameRootClaim.claimant : countered, subgameRootClaim); // Once a subgame is resolved, we percolate the result up the DAG so subsequent calls to // resolveClaim will not need to traverse this subgame. subgameRootClaim.counteredBy = countered; } } } /// @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, 0x20); } /// @inheritdoc IDisputeGame function gameData() external view returns (GameType gameType_, Claim rootClaim_, bytes memory extraData_) { gameType_ = gameType(); rootClaim_ = rootClaim(); extraData_ = extraData(); } //////////////////////////////////////////////////////////////// // MISC EXTERNAL // //////////////////////////////////////////////////////////////// /// @notice Returns the required bond for a given move kind. /// @param _position The position of the bonded interaction. /// @return requiredBond_ The required ETH bond for the given move, in wei. function getRequiredBond(Position _position) public view returns (uint256 requiredBond_) { if (msg.sender == claimData[0].claimant) { // The root claimant does not need to pay a bond. Since we already locked the prize in the pool. // Only the challenger needs to pay the bond. return 0; } uint256 depth = uint256(_position.depth()); if (depth > MAX_GAME_DEPTH) revert GameDepthExceeded(); // Values taken from Big Bonds v1.5 (TM) spec. uint256 assumedBaseFee = 200 gwei; uint256 baseGasCharged = 400_000; uint256 highGasCharged = 300_000_000; // Goal here is to compute the fixed multiplier that will be applied to the base gas // charged to get the required gas amount for the given depth. We apply this multiplier // some `n` times where `n` is the depth of the position. We are looking for some number // that, when multiplied by itself `MAX_GAME_DEPTH` times and then multiplied by the base // gas charged, will give us the maximum gas that we want to charge. // We want to solve for (highGasCharged/baseGasCharged) ** (1/MAX_GAME_DEPTH). // We know that a ** (b/c) is equal to e ** (ln(a) * (b/c)). // We can compute e ** (ln(a) * (b/c)) quite easily with FixedPointMathLib. // Set up a, b, and c. uint256 a = highGasCharged / baseGasCharged; uint256 b = FixedPointMathLib.WAD; uint256 c = MAX_GAME_DEPTH * FixedPointMathLib.WAD; // Compute ln(a). // slither-disable-next-line divide-before-multiply uint256 lnA = uint256(FixedPointMathLib.lnWad(int256(a * FixedPointMathLib.WAD))); // Computes (b / c) with full precision using WAD = 1e18. uint256 bOverC = FixedPointMathLib.divWad(b, c); // Compute e ** (ln(a) * (b/c)) // sMulWad can be used here since WAD = 1e18 maintains the same precision. uint256 numerator = FixedPointMathLib.mulWad(lnA, bOverC); int256 base = FixedPointMathLib.expWad(int256(numerator)); // Compute the required gas amount. int256 rawGas = FixedPointMathLib.powWad(base, int256(depth * FixedPointMathLib.WAD)); uint256 requiredGas = FixedPointMathLib.mulWad(baseGasCharged, uint256(rawGas)); // Compute the required bond. requiredBond_ = assumedBaseFee * requiredGas; } /// @notice Claim the credit belonging to the recipient address. /// @param _recipient The owner and recipient of the credit. function claimCredit(address _recipient) external { // Remove the credit from the recipient prior to performing the external call. uint256 recipientCredit = credit[_recipient]; credit[_recipient] = 0; // Revert if the recipient has no credit to claim. if (recipientCredit == 0) revert NoCreditToClaim(); // Try to withdraw the WETH amount so it can be used here. WETH.withdraw(_recipient, recipientCredit); // Transfer the credit to the recipient. (bool success,) = _recipient.call{value : recipientCredit}(hex""); if (!success) revert BondTransferFailed(); } /// @notice Returns the amount of time elapsed on the potential challenger to `_claimIndex`'s chess clock. Maxes /// out at `MAX_CLOCK_DURATION`. /// @param _claimIndex The index of the subgame root claim. /// @return duration_ The time elapsed on the potential challenger to `_claimIndex`'s chess clock. function getChallengerDuration(uint256 _claimIndex) public view returns (Duration duration_) { // INVARIANT: The game must be in progress to query the remaining time to respond to a given claim. if (status != GameStatus.IN_PROGRESS) { revert GameNotInProgress(); } // Fetch the subgame root claim. ClaimData storage subgameRootClaim = claimData[_claimIndex]; // Fetch the parent of the subgame root's clock, if it exists. Clock parentClock; if (subgameRootClaim.parentIndex != type(uint32).max) { parentClock = claimData[subgameRootClaim.parentIndex].clock; } // Compute the duration elapsed of the potential challenger's clock. uint64 challengeDuration = uint64(parentClock.duration().raw() + (block.timestamp - subgameRootClaim.clock.timestamp().raw())); duration_ = challengeDuration > MAX_CLOCK_DURATION.raw() ? MAX_CLOCK_DURATION : Duration.wrap(challengeDuration); } /// @notice Returns the length of the `claimData` array. function claimDataLen() external view returns (uint256 len_) { len_ = claimData.length; } //////////////////////////////////////////////////////////////// // IMMUTABLE GETTERS // //////////////////////////////////////////////////////////////// /// @notice Returns the absolute prestate of the instruction trace. function absolutePrestate() external view returns (Claim absolutePrestate_) { absolutePrestate_ = ABSOLUTE_PRESTATE; } /// @notice Returns the max game depth. function maxGameDepth() external view returns (uint256 maxGameDepth_) { maxGameDepth_ = MAX_GAME_DEPTH; } /// @notice Returns the split depth. function splitDepth() external view returns (uint256 splitDepth_) { splitDepth_ = SPLIT_DEPTH; } /// @notice Returns the max clock duration. function maxClockDuration() external view returns (Duration maxClockDuration_) { maxClockDuration_ = MAX_CLOCK_DURATION; } /// @notice Returns the clock extension constant. function clockExtension() external view returns (Duration clockExtension_) { clockExtension_ = CLOCK_EXTENSION; } /// @notice Returns the address of the VM. function vm() external view returns (IBigStepper vm_) { vm_ = VM; } /// @notice Returns the WETH contract for holding ETH. function weth() external view returns (IDelayedWETH weth_) { weth_ = WETH; } /// @notice Returns the anchor state registry contract. function scc() external view returns (IMVMStateCommitmentChain scc_) { scc_ = IMVMStateCommitmentChain(ADDRESS_MANAGER.getAddress(SCC_NAME)); } /// @notice Returns the chain ID of the L2 network this contract argues about. function l2ChainId() external view returns (uint256 l2ChainId_) { l2ChainId_ = L2_CHAIN_ID; } //////////////////////////////////////////////////////////////// // HELPERS // //////////////////////////////////////////////////////////////// /// @notice Pays out the bond of a claim to a given recipient. /// @param _recipient The recipient of the bond. /// @param _bonded The claim to pay out the bond of. function _distributeBond(address _recipient, ClaimData storage _bonded) internal { // Set all bits in the bond value to indicate that the bond has been paid out. uint256 bond = _bonded.bond; // Increase the recipient's credit. credit[_recipient] += bond; // Unlock the bond. WETH.unlock(_recipient, bond); } /// @notice Verifies the integrity of an execution bisection subgame's root claim. Reverts if the claim /// is invalid. /// @param _rootClaim The root claim of the execution bisection subgame. function _verifyExecBisectionRoot( Claim _rootClaim, uint256 _parentIdx, Position _parentPos, bool _isAttack ) internal view { // The root claim of an execution trace bisection sub-game must: // 1. Signal that the VM panicked or resulted in an invalid transition if the disputed output root // was made by the opposing party. // 2. Signal that the VM resulted in a valid transition if the disputed output root was made by the same party. // If the move is a defense, the disputed output could have been made by either party. In this case, we // need to search for the parent output to determine what the expected status byte should be. Position disputedLeafPos = Position.wrap(_parentPos.raw() + 1); ClaimData storage disputed = _findTraceAncestor({_pos: disputedLeafPos, _start: _parentIdx, _global: true}); uint8 vmStatus = uint8(_rootClaim.raw()[0]); if (_isAttack || disputed.position.depth() % 2 == SPLIT_DEPTH % 2) { // If the move is an attack, the parent output is always deemed to be disputed. In this case, we only need // to check that the root claim signals that the VM panicked or resulted in an invalid transition. // If the move is a defense, and the disputed output and creator of the execution trace subgame disagree, // the root claim should also signal that the VM panicked or resulted in an invalid transition. if (!(vmStatus == VMStatuses.INVALID.raw() || vmStatus == VMStatuses.PANIC.raw())) { revert UnexpectedRootClaim(_rootClaim); } } else if (vmStatus != VMStatuses.VALID.raw()) { // The disputed output and the creator of the execution trace subgame agree. The status byte should // have signaled that the VM succeeded. revert UnexpectedRootClaim(_rootClaim); } } /// @notice Finds the trace ancestor of a given position within the DAG. /// @param _pos The position to find the trace ancestor claim of. /// @param _start The index to start searching from. /// @param _global Whether or not to search the entire dag or just within an execution trace subgame. If set to /// `true`, and `_pos` is at or above the split depth, this function will revert. /// @return ancestor_ The ancestor claim that commits to the same trace index as `_pos`. function _findTraceAncestor( Position _pos, uint256 _start, bool _global ) internal view returns (ClaimData storage ancestor_) { // Grab the trace ancestor's expected position. Position traceAncestorPos = _global ? _pos.traceAncestor() : _pos.traceAncestorBounded(SPLIT_DEPTH); // Walk up the DAG to find a claim that commits to the same trace index as `_pos`. It is // guaranteed that such a claim exists. ancestor_ = claimData[_start]; while (ancestor_.position.raw() != traceAncestorPos.raw()) { ancestor_ = claimData[ancestor_.parentIndex]; } } /// @notice Finds the starting and disputed output root for a given `ClaimData` within the DAG. This /// `ClaimData` must be below the `SPLIT_DEPTH`. /// @param _start The index within `claimData` of the claim to start searching from. /// @return startingClaim_ The starting output root claim. /// @return startingPos_ The starting output root position. /// @return disputedClaim_ The disputed output root claim. /// @return disputedPos_ The disputed output root position. function _findStartingAndDisputedOutputs(uint256 _start) internal view returns (Claim startingClaim_, Position startingPos_, Claim disputedClaim_, Position disputedPos_) { // Fatch the starting claim. uint256 claimIdx = _start; ClaimData storage claim = claimData[claimIdx]; // If the starting claim's depth is less than or equal to the split depth, we revert as this is UB. if (claim.position.depth() <= SPLIT_DEPTH) revert ClaimAboveSplit(); // We want to: // 1. Find the first claim at the split depth. // 2. Determine whether it was the starting or disputed output for the exec game. // 3. Find the complimentary claim depending on the info from #2 (pre or post). // Walk up the DAG until the ancestor's depth is equal to the split depth. uint256 currentDepth; ClaimData storage execRootClaim = claim; while ((currentDepth = claim.position.depth()) > SPLIT_DEPTH) { uint256 parentIndex = claim.parentIndex; // If we're currently at the split depth + 1, we're at the root of the execution sub-game. // We need to keep track of the root claim here to determine whether the execution sub-game was // started with an attack or defense against the output leaf claim. if (currentDepth == SPLIT_DEPTH + 1) execRootClaim = claim; claim = claimData[parentIndex]; claimIdx = parentIndex; } // Determine whether the start of the execution sub-game was an attack or defense to the output root // above. This is important because it determines which claim is the starting output root and which // is the disputed output root. (Position execRootPos, Position outputPos) = (execRootClaim.position, claim.position); bool wasAttack = execRootPos.parent().raw() == outputPos.raw(); // Determine the starting and disputed output root indices. // 1. If it was an attack, the disputed output root is `claim`, and the starting output root is // elsewhere in the DAG (it must commit to the block # index at depth of `outputPos - 1`). // 2. If it was a defense, the starting output root is `claim`, and the disputed output root is // elsewhere in the DAG (it must commit to the block # index at depth of `outputPos + 1`). if (wasAttack) { // If this is an attack on the first output root (the block directly after the starting // block number), the starting claim nor position exists in the tree. We leave these as // 0, which can be easily identified due to 0 being an invalid Gindex. if (outputPos.indexAtDepth() > 0) { ClaimData storage starting = _findTraceAncestor(Position.wrap(outputPos.raw() - 1), claimIdx, true); (startingClaim_, startingPos_) = (starting.claim, starting.position); } else { startingClaim_ = Claim.wrap(startingOutputRoot.root.raw()); } (disputedClaim_, disputedPos_) = (claim.claim, claim.position); } else { ClaimData storage disputed = _findTraceAncestor(Position.wrap(outputPos.raw() + 1), claimIdx, true); (startingClaim_, startingPos_) = (claim.claim, claim.position); (disputedClaim_, disputedPos_) = (disputed.claim, disputed.position); } } /// @notice Finds the local context hash for a given claim index that is present in an execution trace subgame. /// @param _claimIndex The index of the claim to find the local context hash for. /// @return uuid_ The local context hash. function _findLocalContext(uint256 _claimIndex) internal view returns (Hash uuid_) { (Claim starting, Position startingPos, Claim disputed, Position disputedPos) = _findStartingAndDisputedOutputs(_claimIndex); uuid_ = _computeLocalContext(starting, startingPos, disputed, disputedPos); } /// @notice Computes the local context hash for a set of starting/disputed claim values and positions. /// @param _starting The starting claim. /// @param _startingPos The starting claim's position. /// @param _disputed The disputed claim. /// @param _disputedPos The disputed claim's position. /// @return uuid_ The local context hash. function _computeLocalContext( Claim _starting, Position _startingPos, Claim _disputed, Position _disputedPos ) internal pure returns (Hash uuid_) { // A position of 0 indicates that the starting claim is the absolute prestate. In this special case, // we do not include the starting claim within the local context hash. uuid_ = _startingPos.raw() == 0 ? Hash.wrap(keccak256(abi.encode(_disputed, _disputedPos))) : Hash.wrap(keccak256(abi.encode(_starting, _startingPos, _disputed, _disputedPos))); } /// @notice Emitted when slashing fails event SlashingFailed(address indexed intendedRecipient, bytes reason); }