Coinbase

Coinbase: Multiproof

Cantina Security Report

Organization

@coinbase

Engagement Type

Cantina Reviews

Period

-

Researchers


Findings

Informational

5 findings

4 fixed

1 acknowledged


Informational5 findings

  1. Unconditional Proof Threshold Check in resolve Blocks Normal Bond Recovery When Parent Game Is Invalid

    Severity

    Severity: Informational

    Likelihood: Low

    ×

    Impact: Low

    Submitted by

    Jay


    Description

    The resolve function in AggregateVerifierenforces the proofCount threshold check unconditionally after the branch that sets status = CHALLENGER_WINS when the parent game is blacklisted, retired, or lost. With the planned upgrade to PROOF_THRESHOLD = 2, a game initialized with only one proof would have resolve revert withNotEnoughProofs, rolling back the status change and leaving the game stuck as IN_PROGRESS.

    Since PROOF_THRESHOLD = 2requires both a TEE and ZK proof, a second proof can be submitted via verifyProposalProofbefore calling resolve to satisfy the threshold. The initialization proof already validates the state transition, so the second proof type can be produced and submitted within the 7-day SLOW_FINALIZATION_DELAYwindow.

    The edge case arises if the second proof cannot be submitted within the 7-day window. For example, if a soundness issue is discovered in the ZK verifier through another game causing it to be nullified, no ZK proof can ever be submitted. If the parent game is then blacklisted, the child game needs to resolve as CHALLENGER_WINSbut proofCount remains at 1. The game is permanently stuck asIN_PROGRESS, the bond is unrecoverable through normal operations and requires admin intervention via DelayedWETH, and child games built on top are also blocked from resolving.

    Additionally, the isChallenged bond recipient reassignment executes unconditionally after the parent status branch. When the parent is invalid, resolution is determined entirely by the parent's status, so redirecting the bond to the ZK prover is semantically off. In practice this secondary concern only affects games that were both challenged and have an invalid parent, since challenged games have proofCount of 2 and pass the threshold check regardless.

    Recommendation

    Move the proofCountt threshold check and the isChallenged bond recipient reassignment into the else branch so they only apply when the parent game is valid. When the parent is invalid, resolution should proceed directly to CHALLENGER_WINS and mark resolvedAt without requiring the proof threshold to be met, ensuring bonds are always recoverable regardless of how many proofs the child game has accumulated.

  2. Event Emissions Across Verification Functions Reflect Stale or Incomplete State

    Severity

    Severity: Informational

    Submitted by

    Jay


    Description

    Multiple event emissions in the verification system emit data that does not accurately reflect the final validated state of the operation.

    In NitroEnclaveVerifier, the verify function decodes the raw calldata output into a VerifierJournal, then passes it through _verifyJournal, which may mutate journal.result to a failure status such as RootCertNotTrustedor InvalidTimestamp. However, theAttestationSubmittedevent is emitted using the original raw output bytes rather than the validated journal. Anyone decoding the event log will observe the original unvalidated result value, typically Success, even when _verifyJournal determined the attestation was invalid. The first event parameter correctly reflects the post-validation journal.result, creating a direct contradiction with the third parameter that still carries the stale calldata. The batchVerify function in the same contract does not exhibit this inconsistency, as it emits abi.encode of the fully validated results array after processing each journal through _verifyJournal.

    Additionally, the AttestationSubmittedand BatchAttestationSubmittedevent declarations lack indexed parameters entirely, despite every other event in the same contract that referenceszkCoProcessormarking it as indexed. This inconsistency means off-chain indexers and monitoring systems cannot efficiently filter attestation events by coprocessor type and must instead decode full event data or fall back to transaction calldata parsing.

    The NotImplemented error declared in NitroEnclaveVerifier is also unused anywhere in the contract, adding unnecessary surface area to the interface.

    Recommendation

    • Update the verify function to emit the validated journal in the AttestationSubmittedevent rather than the raw calldata, replacing the third parameter withabi.encodeof the validated journal to match the pattern already established by batchVerify.

    • Add indexed to the zkCoProcessorparameter on both the AttestationSubmittedand BatchAttestationSubmitted event declarations for consistency with the rest of the contract.

    • Remove the unused NotImplementederror declaration.

  3. Inaccurate NatSpec and Dead Code

    Severity

    Severity: Informational

    Submitted by

    Jay


    Description

    The challenge function inAggregateVerifiercontains a NatSpec comment stating that the expected resolution time can no longer be increased after both proof types have been submitted. This is inaccurate. After a challenge, if the ZK proof is subsequently nullified, _increaseExpectedResolution can and does extend the expected resolution time beyond what was originally set during the challenge. This misleading comment could lead developers or auditors to make incorrect assumptions about the finalization timing guarantees of the system.

    Separately, theL2_CHAIN_IDimmutable variable is assigned in the constructor but is never read anywhere in the contract. It is not included in the journal hash for either_verifyTeeProofor _verifyZkProof, even though CONFIG_HASHand the image hashes are. WhileCONFIG_HASH already commits to the chain ID in its off-chain preimage, the presence of an unused immutable variable is misleading, as it suggests proofs are explicitly bound to a specific L2 chain within this contract when they are not. For reference, FaultDisputeGameactively uses its own L2_CHAIN_IDfor preimage oracle data, making the omission here appear unintentional.

    Recommendation

    Correct the NatSpec comment in the challenge function to accurately reflect that the expected resolution time can still be increased if the ZK proof is later nullified. Remove the unused L2_CHAIN_IDimmutable variable from AggregateVerifier to eliminate dead code and avoid implying chain-specific binding that does not actually exist at the contract level.

  4. Duplicate code should be put in a helper function to enable code reuse

    Severity

    Severity: Informational

    Submitted by

    0xicingdeath


    Summary

    Finding Description

    The _increaseExpectedResolution function and the decreaseExpectedResolution function both have logic to adjust the delay to either a fast or slow finalization, and then to set expectedResolution. This should be put into a helper function that returns the delay, and sets the expectedResolution. In doing so, this will ensure that any logic adjusted will only need to be changed in one function, as opposed to multiple parts of the code.

    Recommendation

    Create a helper function that both functions use.

    Coinbase

    Created a helper function as per recommendations.

  5. [Appendix] Test Coverage Improvements: AggregateVerifier.challenge(), resolve(), claimcredit()

    State

    Acknowledged

    Severity

    Severity: Informational

    Submitted by

    Jay


    Overview

    The existing test suite for AggregateVerifier.sol had gaps in coverage for challenge(), resolve(), and claimCredit(). This appendix documents the tests written to close those gaps, organized by function.

    ChallengeInvariants.t.sol : challenge()

    TestDetailsStatus
    testChallenge_AfterGameOverButBeforeResolve_Succeedschallenge() succeeds after gameOver (no gameOver check), resets expectedResolution to now + 7 days
    testChallenge_GriefingScenario_ExtendsResolutionBy7DaysLate challenge extends total delay from ~7 to ~14 days
    testVerifyProposalProof_AfterGameOver_RevertsverifyProposalProof() reverts with GameOver() — asymmetry with challenge()
    testChallenge_DoubleChallengeFromSameAddress_RevertsSecond challenge from same address reverts AlreadyProven(ZK), proofCount stays 2
    testChallenge_DoubleChallengeFromDifferentAddress_RevertsSecond challenge from different address also reverts, original zkProver preserved
    testChallenge_BlocksSubsequentVerifyProposalProof_RevertsverifyProposalProof(ZK) reverts after challenge() — no double-counting across entry points
    testChallenge_ThenSubmitAnotherTEE_RevertsTEE slot stays occupied after ZK challenge, second TEE reverts AlreadyProven(TEE)
    testChallenge_WithSameIntermediateRoot_RevertsReverts IntermediateRootSameAsProposed when challenger uses same root as proposer
    testChallenge_WithSameIntermediateRootAtIndexZero_RevertsSame-root rejection at index 0
    testChallenge_WithDifferentIntermediateRoot_SucceedsDifferent root at same index succeeds, sets counteredIndex and resets expectedResolution
    testChallenge_AtIndexZero_UsesParentStartingRoot_SucceedsIndex 0 exercises startingRoot = startingOutputRoot.root code path, resolves CHALLENGER_WINS
    testChallenge_AtMiddleIndex_UsesPreviousIntermediateRoot_SucceedsMiddle index exercises startingRoot = intermediateOutputRoot(index - 1) path
    testChallenge_AtIndexZero_WithSameRoot_RevertsSame-root check works at index 0 despite different startingRoot path
    testChallenge_WithOutOfBoundsIndex_RevertsReverts InvalidIntermediateRootIndex for index one past last valid
    testChallenge_WithMaxUint256Index_RevertsReverts InvalidIntermediateRootIndex for type(uint256).max — overflow guard

    Resolve.t.sol : resolve()

    TestDetailsStatus
    testResolve_ParentGameNotResolved_RevertsReverts ParentGameNotResolved when parent game is still IN_PROGRESS
    testResolve_ParentBlacklisted_ForcesChallengerWinsBlacklisted parent forces CHALLENGER_WINS, bypassing gameOver check
    testResolve_AlreadyResolved_RevertsReverts ClaimAlreadyResolved on double resolve
    testResolve_GameNotOver_RevertsReverts GameNotOver before expectedResolution has passed
    testResolve_Unchallenged_DefenderWinsUnchallenged game resolves as DEFENDER_WINS, bond stays with creator
    testResolve_Challenged_ChallengerWinsChallenged game resolves as CHALLENGER_WINS, bond redirected to ZK prover

    ClaimCredit.t.sol : claimCredit()

    TestDetailsStatus
    testClaimCredit_BeforeResolve_RevertsReverts GameNotResolved when resolvedAt is 0 (expectedResolution != uint64.max path)
    testClaimCredit_NullifiedGame_Before14Days_RevertsReverts GameNotOver on uint64.max path before 14 days from creation
    testClaimCredit_NullifiedGame_After14Days_SucceedsNullified game can unlock and claim bond after 14 days
    testClaimCredit_AlreadyClaimed_RevertsReverts NoCreditToClaim on third call after bond already claimed
    testClaimCredit_DefenderWins_FullLifecycleFull cycle: 2 proofs, fast finalization, DEFENDER_WINS, bond returned to creator
    testClaimCredit_ChallengerWins_FullLifecycleFull cycle: challenge, CHALLENGER_WINS, bond paid to ZK prover
    testClaimCredit_ParentBlacklisted_BondToCreatorParent blacklisted resolve + bond stays with creator (no challenge() was called)

    Test Code

    Challenge.t.sol

    // SPDX-License-Identifier: MITpragma solidity 0.8.15;
    import { ClaimAlreadyResolved } from "src/dispute/lib/Errors.sol";import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol";import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol";import { Claim, GameStatus, Hash, Timestamp } from "src/dispute/lib/Types.sol";
    import { AggregateVerifier } from "src/multiproof/AggregateVerifier.sol";import { Verifier } from "src/multiproof/Verifier.sol";
    import { BaseTest } from "./BaseTest.t.sol";
    contract ChallengeInvariantsTest is BaseTest {    function testChallenge_AfterGameOverButBeforeResolve_Succeeds() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            uint64 initialExpectedResolution = game.expectedResolution().raw();        assertEq(game.proofCount(), 1);        assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS));
            vm.warp(initialExpectedResolution + 1);        assertTrue(game.gameOver());        assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS));
            Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk")));        bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK);
            vm.prank(ZK_PROVER);        game.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw());
            assertEq(game.proofCount(), 2);        assertEq(game.zkProver(), ZK_PROVER);
            uint64 newExpectedResolution = game.expectedResolution().raw();        assertEq(newExpectedResolution, uint64(block.timestamp + 7 days));        assertTrue(newExpectedResolution > initialExpectedResolution);        assertFalse(game.gameOver());
            vm.warp(newExpectedResolution + 1);        assertTrue(game.gameOver());        game.resolve();
            assertEq(uint8(game.status()), uint8(GameStatus.CHALLENGER_WINS));        assertEq(game.bondRecipient(), ZK_PROVER);    }
        function testChallenge_GriefingScenario_ExtendsResolutionBy7Days() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            uint256 gameCreationTime = block.timestamp;        uint64 initialExpectedResolution = game.expectedResolution().raw();
            vm.warp(initialExpectedResolution + 1);        assertTrue(game.gameOver());
            Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk")));        bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK);
            vm.prank(ZK_PROVER);        game.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw());
            uint64 newExpectedResolution = game.expectedResolution().raw();        uint256 totalDelay = newExpectedResolution - gameCreationTime;
            assertTrue(totalDelay > 13 days);        assertFalse(game.gameOver());
            vm.warp(newExpectedResolution + 1);        game.resolve();        assertEq(uint8(game.status()), uint8(GameStatus.CHALLENGER_WINS));    }
        function testVerifyProposalProof_AfterGameOver_Reverts() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            vm.warp(game.expectedResolution().raw() + 1);        assertTrue(game.gameOver());
            bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK);
            vm.expectRevert(AggregateVerifier.GameOver.selector);        vm.prank(ZK_PROVER);        game.verifyProposalProof(zkProof);    }
        function testChallenge_DoubleChallengeFromSameAddress_Reverts() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk1")));        bytes memory zkProof1 = _generateProof("zk-proof-1", AggregateVerifier.ProofType.ZK);
            vm.prank(ZK_PROVER);        game.challenge(zkProof1, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw());        assertEq(game.proofCount(), 2);        assertEq(game.zkProver(), ZK_PROVER);
            Claim rootClaim3 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk2")));        bytes memory zkProof2 = _generateProof("zk-proof-2", AggregateVerifier.ProofType.ZK);
            vm.expectRevert(            abi.encodeWithSelector(AggregateVerifier.AlreadyProven.selector, AggregateVerifier.ProofType.ZK)        );        vm.prank(ZK_PROVER);        game.challenge(zkProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim3.raw());
            assertEq(game.proofCount(), 2);    }
        function testChallenge_DoubleChallengeFromDifferentAddress_Reverts() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk1")));        bytes memory zkProof1 = _generateProof("zk-proof-1", AggregateVerifier.ProofType.ZK);
            vm.prank(ZK_PROVER);        game.challenge(zkProof1, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw());
            Claim rootClaim3 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk2")));        bytes memory zkProof2 = _generateProof("zk-proof-2", AggregateVerifier.ProofType.ZK);
            vm.expectRevert(            abi.encodeWithSelector(AggregateVerifier.AlreadyProven.selector, AggregateVerifier.ProofType.ZK)        );        vm.prank(ATTACKER);        game.challenge(zkProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim3.raw());
            assertEq(game.proofCount(), 2);        assertEq(game.zkProver(), ZK_PROVER);    }
        function testChallenge_BlocksSubsequentVerifyProposalProof_Reverts() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk1")));        bytes memory zkProof1 = _generateProof("zk-proof-1", AggregateVerifier.ProofType.ZK);
            vm.prank(ZK_PROVER);        game.challenge(zkProof1, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw());
            bytes memory zkProof2 = _generateProof("zk-proof-2", AggregateVerifier.ProofType.ZK);
            vm.expectRevert(            abi.encodeWithSelector(AggregateVerifier.AlreadyProven.selector, AggregateVerifier.ProofType.ZK)        );        vm.prank(ATTACKER);        game.verifyProposalProof(zkProof2);
            assertEq(game.proofCount(), 2);    }
        function testChallenge_ThenSubmitAnotherTEE_Reverts() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk")));        bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK);
            vm.prank(ZK_PROVER);        game.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw());
            bytes memory teeProof2 = _generateProof("tee-proof-2", AggregateVerifier.ProofType.TEE);
            vm.expectRevert(            abi.encodeWithSelector(AggregateVerifier.AlreadyProven.selector, AggregateVerifier.ProofType.TEE)        );        vm.prank(ATTACKER);        game.verifyProposalProof(teeProof2);
            assertEq(game.proofCount(), 2);    }
        function testChallenge_WithSameIntermediateRoot_Reverts() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            uint256 lastIndex = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1;        bytes32 proposedRoot = game.intermediateOutputRoot(lastIndex);        bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK);
            vm.expectRevert(AggregateVerifier.IntermediateRootSameAsProposed.selector);        vm.prank(ZK_PROVER);        game.challenge(zkProof, lastIndex, proposedRoot);
            assertEq(game.proofCount(), 1);        assertEq(game.zkProver(), address(0));        assertEq(game.counteredByIntermediateRootIndexPlusOne(), 0);    }
        function testChallenge_WithSameIntermediateRootAtIndexZero_Reverts() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            bytes32 proposedRootAtZero = game.intermediateOutputRoot(0);        bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK);
            vm.expectRevert(AggregateVerifier.IntermediateRootSameAsProposed.selector);        vm.prank(ZK_PROVER);        game.challenge(zkProof, 0, proposedRootAtZero);
            assertEq(game.proofCount(), 1);        assertEq(game.zkProver(), address(0));    }
        function testChallenge_WithDifferentIntermediateRoot_Succeeds() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            uint256 lastIndex = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1;        bytes32 proposedRoot = game.intermediateOutputRoot(lastIndex);        bytes32 differentRoot = keccak256(abi.encode("different-root"));        assertTrue(differentRoot != proposedRoot);
            bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK);
            vm.prank(ZK_PROVER);        game.challenge(zkProof, lastIndex, differentRoot);
            assertEq(game.proofCount(), 2);        assertEq(game.zkProver(), ZK_PROVER);        assertEq(game.counteredByIntermediateRootIndexPlusOne(), lastIndex + 1);        assertEq(game.expectedResolution().raw(), uint64(block.timestamp + 7 days));    }
        function testChallenge_AtIndexZero_UsesParentStartingRoot_Succeeds() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            bytes32 proposedRootAtZero = game.intermediateOutputRoot(0);        bytes32 differentRoot = keccak256(abi.encode("different-root-for-index-0"));        assertTrue(differentRoot != proposedRootAtZero);
            bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK);
            vm.prank(ZK_PROVER);        game.challenge(zkProof, 0, differentRoot);
            assertEq(game.proofCount(), 2);        assertEq(game.zkProver(), ZK_PROVER);        assertEq(game.counteredByIntermediateRootIndexPlusOne(), 1);        assertEq(game.expectedResolution().raw(), uint64(block.timestamp + 7 days));
            vm.warp(game.expectedResolution().raw() + 1);        game.resolve();        assertEq(uint8(game.status()), uint8(GameStatus.CHALLENGER_WINS));        assertEq(game.bondRecipient(), ZK_PROVER);    }
        function testChallenge_AtMiddleIndex_UsesPreviousIntermediateRoot_Succeeds() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            uint256 middleIndex = 5;        bytes32 proposedRootAtMiddle = game.intermediateOutputRoot(middleIndex);        bytes32 differentRoot = keccak256(abi.encode("different-root-for-middle-index"));        assertTrue(differentRoot != proposedRootAtMiddle);
            bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK);
            vm.prank(ZK_PROVER);        game.challenge(zkProof, middleIndex, differentRoot);
            assertEq(game.proofCount(), 2);        assertEq(game.zkProver(), ZK_PROVER);        assertEq(game.counteredByIntermediateRootIndexPlusOne(), middleIndex + 1);
            vm.warp(game.expectedResolution().raw() + 1);        game.resolve();        assertEq(uint8(game.status()), uint8(GameStatus.CHALLENGER_WINS));    }
        function testChallenge_AtIndexZero_WithSameRoot_Reverts() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            bytes32 proposedRootAtZero = game.intermediateOutputRoot(0);        bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK);
            vm.expectRevert(AggregateVerifier.IntermediateRootSameAsProposed.selector);        vm.prank(ZK_PROVER);        game.challenge(zkProof, 0, proposedRootAtZero);
            assertEq(game.proofCount(), 1);        assertEq(game.zkProver(), address(0));        assertEq(game.counteredByIntermediateRootIndexPlusOne(), 0);    }
        function testChallenge_WithOutOfBoundsIndex_Reverts() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            uint256 outOfBoundsIndex = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL;
            bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK);
            vm.expectRevert(AggregateVerifier.InvalidIntermediateRootIndex.selector);        vm.prank(ZK_PROVER);        game.challenge(zkProof, outOfBoundsIndex, keccak256("doesnt-matter"));    }
        function testChallenge_WithMaxUint256Index_Reverts() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK);
            vm.expectRevert(AggregateVerifier.InvalidIntermediateRootIndex.selector);        vm.prank(ZK_PROVER);        game.challenge(zkProof, type(uint256).max, keccak256("doesnt-matter"));    }}

    Resolve.t.sol

    // SPDX-License-Identifier: MITpragma solidity 0.8.15;
    import { ClaimAlreadyResolved } from "src/dispute/lib/Errors.sol";import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol";import { Claim, GameStatus, Hash, Timestamp } from "src/dispute/lib/Types.sol";
    import { AggregateVerifier } from "src/multiproof/AggregateVerifier.sol";
    import { BaseTest } from "./BaseTest.t.sol";
    contract ResolveTest is BaseTest {    function testResolve_ParentGameNotResolved_Reverts() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory parentProof = _generateProof("parent-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier parentGame =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, parentProof);
            uint256 parentGameIndex = factory.gameCount() - 1;        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2")));        bytes memory childProof = _generateProof("child-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier childGame =            _createAggregateVerifierGame(                TEE_PROVER, rootClaim2, currentL2BlockNumber, uint32(parentGameIndex), childProof            );
            vm.warp(block.timestamp + 7 days);
            assertEq(uint8(parentGame.status()), uint8(GameStatus.IN_PROGRESS));
            vm.expectRevert(AggregateVerifier.ParentGameNotResolved.selector);        childGame.resolve();    }
        function testResolve_ParentBlacklisted_ForcesChallengerWins() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory parentProof = _generateProof("parent-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier parentGame =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, parentProof);
            uint256 parentGameIndex = factory.gameCount() - 1;        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2")));        bytes memory childProof = _generateProof("child-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier childGame =            _createAggregateVerifierGame(                TEE_PROVER, rootClaim2, currentL2BlockNumber, uint32(parentGameIndex), childProof            );
            anchorStateRegistry.blacklistDisputeGame(IDisputeGame(address(parentGame)));
            childGame.resolve();
            assertEq(uint8(childGame.status()), uint8(GameStatus.CHALLENGER_WINS));        assertEq(childGame.bondRecipient(), TEE_PROVER);    }
        function testResolve_AlreadyResolved_Reverts() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory proof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim, currentL2BlockNumber, type(uint32).max, proof);
            vm.warp(block.timestamp + 7 days);        game.resolve();
            vm.expectRevert(ClaimAlreadyResolved.selector);        game.resolve();    }
        function testResolve_GameNotOver_Reverts() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory proof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim, currentL2BlockNumber, type(uint32).max, proof);
            vm.warp(block.timestamp + 1 days);        assertFalse(game.gameOver());
            vm.expectRevert(AggregateVerifier.GameNotOver.selector);        game.resolve();    }
        function testResolve_Unchallenged_DefenderWins() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory proof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim, currentL2BlockNumber, type(uint32).max, proof);
            vm.warp(block.timestamp + 7 days);        GameStatus result = game.resolve();
            assertEq(uint8(result), uint8(GameStatus.DEFENDER_WINS));        assertEq(uint8(game.status()), uint8(GameStatus.DEFENDER_WINS));        assertEq(game.bondRecipient(), TEE_PROVER);        assertTrue(game.resolvedAt().raw() > 0);    }
        function testResolve_Challenged_ChallengerWins() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk")));        bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK);
            vm.prank(ZK_PROVER);        game.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw());
            vm.warp(game.expectedResolution().raw() + 1);        GameStatus result = game.resolve();
            assertEq(uint8(result), uint8(GameStatus.CHALLENGER_WINS));        assertEq(game.bondRecipient(), ZK_PROVER);    }}

    ClaimCredit.t.sol

    // SPDX-License-Identifier: MITpragma solidity 0.8.15;
    import { GameNotResolved, NoCreditToClaim } from "src/dispute/lib/Errors.sol";import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol";import { Claim, GameStatus, Hash, Timestamp } from "src/dispute/lib/Types.sol";
    import { AggregateVerifier } from "src/multiproof/AggregateVerifier.sol";
    import { BaseTest } from "./BaseTest.t.sol";
    contract ClaimCreditTest is BaseTest {    function testClaimCredit_BeforeResolve_Reverts() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory proof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim, currentL2BlockNumber, type(uint32).max, proof);
            assertTrue(game.expectedResolution().raw() != type(uint64).max);        assertEq(game.resolvedAt().raw(), 0);
            vm.expectRevert(GameNotResolved.selector);        game.claimCredit();    }
        function testClaimCredit_NullifiedGame_Before14Days_Reverts() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee1")));        bytes memory teeProof1 = _generateProof("tee-proof-1", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof1);
            Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2")));        bytes memory teeProof2 = _generateProof("tee-proof-2", AggregateVerifier.ProofType.TEE);        game.nullify(teeProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw());
            assertEq(game.expectedResolution().raw(), type(uint64).max);
            vm.warp(block.timestamp + 13 days);
            vm.expectRevert(AggregateVerifier.GameNotOver.selector);        game.claimCredit();    }
        function testClaimCredit_NullifiedGame_After14Days_Succeeds() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee1")));        bytes memory teeProof1 = _generateProof("tee-proof-1", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof1);
            Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2")));        bytes memory teeProof2 = _generateProof("tee-proof-2", AggregateVerifier.ProofType.TEE);        game.nullify(teeProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw());
            vm.warp(block.timestamp + 14 days);
            game.claimCredit();        assertTrue(game.bondUnlocked());        assertFalse(game.bondClaimed());
            vm.warp(block.timestamp + DELAYED_WETH_DELAY);        uint256 balanceBefore = TEE_PROVER.balance;        game.claimCredit();
            assertTrue(game.bondClaimed());        assertEq(TEE_PROVER.balance, balanceBefore + INIT_BOND);    }
        function testClaimCredit_AlreadyClaimed_Reverts() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory proof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim, currentL2BlockNumber, type(uint32).max, proof);
            vm.warp(block.timestamp + 7 days);        game.resolve();
            game.claimCredit();        vm.warp(block.timestamp + DELAYED_WETH_DELAY);        game.claimCredit();
            assertTrue(game.bondClaimed());
            vm.expectRevert(NoCreditToClaim.selector);        game.claimCredit();    }
        function testClaimCredit_DefenderWins_FullLifecycle() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);        bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim, currentL2BlockNumber, type(uint32).max, teeProof);
            _provideProof(game, ZK_PROVER, zkProof);        assertEq(game.proofCount(), 2);
            vm.warp(block.timestamp + 1 days);        game.resolve();        assertEq(uint8(game.status()), uint8(GameStatus.DEFENDER_WINS));        assertEq(game.bondRecipient(), TEE_PROVER);
            uint256 balanceBefore = TEE_PROVER.balance;
            game.claimCredit();        vm.warp(block.timestamp + DELAYED_WETH_DELAY);        game.claimCredit();
            assertEq(TEE_PROVER.balance, balanceBefore + INIT_BOND);        assertEq(delayedWETH.balanceOf(address(game)), 0);    }
        function testClaimCredit_ChallengerWins_FullLifecycle() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier game =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof);
            Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk")));        bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK);
            vm.prank(ZK_PROVER);        game.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw());
            vm.warp(game.expectedResolution().raw() + 1);        game.resolve();        assertEq(uint8(game.status()), uint8(GameStatus.CHALLENGER_WINS));        assertEq(game.bondRecipient(), ZK_PROVER);
            uint256 balanceBefore = ZK_PROVER.balance;
            game.claimCredit();        vm.warp(block.timestamp + DELAYED_WETH_DELAY);        game.claimCredit();
            assertEq(ZK_PROVER.balance, balanceBefore + INIT_BOND);        assertEq(delayedWETH.balanceOf(address(game)), 0);    }
        function testClaimCredit_ParentBlacklisted_BondToCreator() public {        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee")));        bytes memory parentProof = _generateProof("parent-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier parentGame =            _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, parentProof);
            uint256 parentGameIndex = factory.gameCount() - 1;        currentL2BlockNumber += BLOCK_INTERVAL;
            Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2")));        bytes memory childProof = _generateProof("child-proof", AggregateVerifier.ProofType.TEE);
            AggregateVerifier childGame =            _createAggregateVerifierGame(                TEE_PROVER, rootClaim2, currentL2BlockNumber, uint32(parentGameIndex), childProof            );
            anchorStateRegistry.blacklistDisputeGame(IDisputeGame(address(parentGame)));        childGame.resolve();
            assertEq(uint8(childGame.status()), uint8(GameStatus.CHALLENGER_WINS));        assertEq(childGame.bondRecipient(), TEE_PROVER);
            uint256 balanceBefore = TEE_PROVER.balance;        childGame.claimCredit();        vm.warp(block.timestamp + DELAYED_WETH_DELAY);        childGame.claimCredit();
            assertEq(TEE_PROVER.balance, balanceBefore + INIT_BOND);    }}