Organization
- @coinbase
Engagement Type
Cantina Reviews
Period
-
Repositories
Researchers
Findings
Informational
5 findings
4 fixed
1 acknowledged
Informational5 findings
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 theproofCountthreshold check unconditionally after the branch that setsstatus = CHALLENGER_WINSwhen the parent game is blacklisted, retired, or lost. With the planned upgrade toPROOF_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 viaverifyProposalProofbefore 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 the7-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_WINSbutproofCountremains at 1. The game is permanently stuck asIN_PROGRESS,the bond is unrecoverable through normal operations and requires admin intervention viaDelayedWETH, 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
proofCountof 2 and pass the threshold check regardless.Recommendation
Move the
proofCounttthreshold check and theisChallengedbond 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 toCHALLENGER_WINSand 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.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 aVerifierJournal, then passes it through_verifyJournal,which may mutate journal.result to a failure status such asRootCertNotTrustedorInvalidTimestamp. 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_verifyJournaldetermined 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
AttestationSubmittedandBatchAttestationSubmittedevent 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
NitroEnclaveVerifieris 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 bybatchVerify. -
Add indexed to the
zkCoProcessorparameter on both theAttestationSubmittedandBatchAttestationSubmittedevent declarations for consistency with the rest of the contract. -
Remove the unused
NotImplementederror declaration.
Inaccurate NatSpec and Dead Code
Severity
- Severity: Informational
Submitted by
Jay
Description
The challenge function in
AggregateVerifiercontains 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,_increaseExpectedResolutioncan 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, the
L2_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 thoughCONFIG_HASHand the image hashes are. WhileCONFIG_HASHalready 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 ownL2_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 fromAggregateVerifierto eliminate dead code and avoid implying chain-specific binding that does not actually exist at the contract level.Duplicate code should be put in a helper function to enable code reuse
Severity
- Severity: Informational
Submitted by
0xicingdeath
Summary
Finding Description
The
_increaseExpectedResolutionfunction and thedecreaseExpectedResolutionfunction both have logic to adjust thedelayto either a fast or slow finalization, and then to setexpectedResolution. This should be put into a helper function that returns thedelay, and sets theexpectedResolution. 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.
[Appendix] Test Coverage Improvements: AggregateVerifier.challenge(), resolve(), claimcredit()
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Jay
Overview
The existing test suite for
AggregateVerifier.solhad gaps in coverage forchallenge(),resolve(), andclaimCredit(). This appendix documents the tests written to close those gaps, organized by function.ChallengeInvariants.t.sol: challenge()Test Details Status 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 IntermediateRootSameAsProposedwhen 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.rootcode 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 InvalidIntermediateRootIndexfor index one past last valid✅ testChallenge_WithMaxUint256Index_RevertsReverts InvalidIntermediateRootIndexfor type(uint256).max — overflow guard✅ Resolve.t.sol: resolve()Test Details Status testResolve_ParentGameNotResolved_RevertsReverts ParentGameNotResolvedwhen parent game is still IN_PROGRESS✅ testResolve_ParentBlacklisted_ForcesChallengerWinsBlacklisted parent forces CHALLENGER_WINS, bypassing gameOver check ✅ testResolve_AlreadyResolved_RevertsReverts ClaimAlreadyResolvedon double resolve✅ testResolve_GameNotOver_RevertsReverts GameNotOverbefore 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()Test Details Status testClaimCredit_BeforeResolve_RevertsReverts GameNotResolvedwhen resolvedAt is 0 (expectedResolution != uint64.max path)✅ testClaimCredit_NullifiedGame_Before14Days_RevertsReverts GameNotOveron uint64.max path before 14 days from creation✅ testClaimCredit_NullifiedGame_After14Days_SucceedsNullified game can unlock and claim bond after 14 days ✅ testClaimCredit_AlreadyClaimed_RevertsReverts NoCreditToClaimon 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); }}