Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9ccc54d
test: add ZK DGF tests
0xChin Mar 31, 2026
f868731
test: restructure ZK dispute game factory tests into dedicated contracts
0xChin Apr 1, 2026
3946a3f
test: add bond overpayment scenario
0xChin Apr 1, 2026
34ff082
chore: pre-pr
0xChin Apr 1, 2026
661f1c6
test: add ZK dispute game tests for AnchorStateRegistry
0xChin Apr 1, 2026
84923be
chore: run just pr
0xChin Apr 1, 2026
c15f8c5
Merge branch 'develop' into test/zk-dispute-game-factory-and-registry
0xChin Apr 1, 2026
a02e5d6
refactor: use vars instead of magic numbers
0xChin Apr 1, 2026
107747f
test: do assertions over exact values
0xChin Apr 1, 2026
818ca5b
refactor: add zkCreateParams helper function for reused logic
0xChin Apr 1, 2026
27d0889
test: check findLatestGames works if n > gameCount
0xChin Apr 1, 2026
dfe8893
test: check notRegistered path in ASR ZKDisputeGame test
0xChin Apr 1, 2026
38a5518
test: add fuzz testing
0xChin Apr 2, 2026
b864e08
test: add more fuzz tests
0xChin Apr 2, 2026
e1d31f8
refactor: use IDisputeGame interface
0xChin Apr 2, 2026
46e6ce0
refactor: add reused vars/logic in init contract
0xChin Apr 2, 2026
a651fe6
chore: run just pr
0xChin Apr 2, 2026
4b80e5b
fix: just-upgrade CI failing
0xChin Apr 2, 2026
c41aa6f
chore: run just pr
0xChin Apr 2, 2026
e8910df
Merge branch 'develop' into test/zk-dispute-game-factory-and-registry
0xChin Apr 3, 2026
f64a2c6
feat: remove skip
ashitakah Apr 3, 2026
92e894d
fix(ci): resource-intensive test breaking CI
0xChin Apr 3, 2026
e97e928
test: increase test coverage in ASR
0xChin Apr 3, 2026
9d6cb9f
refactor: remove redundant oneliner
0xChin Apr 3, 2026
1a786ef
test: ensure same state after revert in ASR
0xChin Apr 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
333 changes: 332 additions & 1 deletion packages/contracts-bedrock/test/dispute/AnchorStateRegistry.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pragma solidity ^0.8.15;
import { BaseFaultDisputeGame_TestInit, _changeClaimStatus } from "test/dispute/FaultDisputeGame.t.sol";

// Libraries
import { GameType, GameTypes, GameStatus, Hash, Claim, VMStatuses, Proposal } from "src/dispute/lib/Types.sol";
import { GameType, GameTypes, GameStatus, Hash, Claim, Duration, VMStatuses, Proposal } from "src/dispute/lib/Types.sol";
import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol";

// Interfaces
Expand All @@ -14,6 +14,7 @@ import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol";
import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol";
import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol";
import { IProxyAdminOwnedBase } from "interfaces/universal/IProxyAdminOwnedBase.sol";
import { DevFeatures } from "src/libraries/DevFeatures.sol";

/// @title AnchorStateRegistry_TestInit
/// @notice Reusable test initialization for `AnchorStateRegistry` tests.
Expand Down Expand Up @@ -1374,3 +1375,333 @@ contract AnchorStateRegistry_SetAnchorState_Test is AnchorStateRegistry_TestInit
anchorStateRegistry.setAnchorState(gameProxy);
}
}

/// @title AnchorStateRegistry_ZkDisputeGame_TestInit
/// @notice Reusable test initialization for ZKDisputeGame AnchorStateRegistry tests.
abstract contract AnchorStateRegistry_ZkDisputeGame_TestInit is AnchorStateRegistry_TestInit {
IDisputeGame zkGameProxy;
uint256 zkL2SequenceNumber;

function setUp() public virtual override {
super.setUp();

skipIfDevFeatureDisabled(DevFeatures.ZK_DISPUTE_GAME);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every this is testes when zk dispute game is enabled. Maybe create one to check that reverts if the flag is disabled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the flag disabled, the ZK game implementation wouldn't be deployed or registered in the factory so there's no zkGameProxy to test against. The ASR and factory are game-type agnostic; they don't check the feature flag themselves. The flag only gates deployment at the OPCM level, which is already covered by test_upgrade_enableZKGameWithoutDevFeature_reverts in the OPCM tests. At the ASR/factory level, "flag disabled" just means "no ZK game exists," which isn't a ZK-specific scenario worth testing here.


// Register ZK game implementation and set it as the respected game type.
setupZKDisputeGame(
ZKDisputeGameParams({
maxChallengeDuration: Duration.wrap(3.5 days),
maxProveDuration: Duration.wrap(12 hours),
absolutePrestate: keccak256("absolutePrestate"),
challengerBond: 1 ether
})
);

// Get anchor state to pick a valid l2SequenceNumber.
(, uint256 anchorL2SeqNum) = anchorStateRegistry.getAnchorRoot();
zkL2SequenceNumber = anchorL2SeqNum + 2000;

// Create a ZK game via the factory.
Claim rootClaim_ = changeClaimStatus(Claim.wrap(keccak256("zkRootClaim")), VMStatuses.INVALID);
bytes memory extraData_ = abi.encodePacked(zkL2SequenceNumber, type(uint32).max);

address proposer = makeAddr("zkProposer");
vm.deal(proposer, 1 ether);
vm.warp(block.timestamp + 1000);

vm.prank(proposer);
zkGameProxy = disputeGameFactory.create{ value: 1 ether }(GameTypes.ZK_DISPUTE_GAME, rootClaim_, extraData_);
}

/// @notice Mocks the ZK game as a valid, resolved, finalized game.
function _mockZkGameAsValid() internal {
vm.mockCall(address(zkGameProxy), abi.encodeCall(zkGameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS));
vm.mockCall(
address(zkGameProxy), abi.encodeCall(zkGameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true)
);
vm.mockCall(address(zkGameProxy), abi.encodeCall(zkGameProxy.resolvedAt, ()), abi.encode(block.timestamp));
vm.mockCall(
address(zkGameProxy), abi.encodeCall(zkGameProxy.l2SequenceNumber, ()), abi.encode(zkL2SequenceNumber)
);
vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1);
}
}

/// @title AnchorStateRegistry_SetAnchorState_ZkDisputeGame_Test
/// @notice Tests the `setAnchorState` function with ZKDisputeGame.
contract AnchorStateRegistry_SetAnchorState_ZkDisputeGame_Test is AnchorStateRegistry_ZkDisputeGame_TestInit {
/// @notice Tests that a valid ZK game can update the anchor state.
function testFuzz_setAnchorState_validNewerState_succeeds(uint256 _l2SequenceNumber) public {
(, uint256 anchorL2SeqNum) = anchorStateRegistry.getAnchorRoot();
_l2SequenceNumber = bound(_l2SequenceNumber, anchorL2SeqNum + 1, type(uint256).max);

vm.mockCall(address(zkGameProxy), abi.encodeCall(zkGameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS));
vm.mockCall(
address(zkGameProxy), abi.encodeCall(zkGameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true)
);
vm.mockCall(address(zkGameProxy), abi.encodeCall(zkGameProxy.resolvedAt, ()), abi.encode(block.timestamp));
vm.mockCall(
address(zkGameProxy), abi.encodeCall(zkGameProxy.l2SequenceNumber, ()), abi.encode(_l2SequenceNumber)
);
vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1);

vm.prank(address(zkGameProxy));
vm.expectEmit(address(anchorStateRegistry));
emit AnchorUpdated(IFaultDisputeGame(address(zkGameProxy)));
anchorStateRegistry.setAnchorState(zkGameProxy);

// Confirm anchor state updated to ZK game's claim.
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot();
assertEq(l2BlockNumber, _l2SequenceNumber);
assertEq(root.raw(), zkGameProxy.rootClaim().raw());

// Confirm anchor game is the ZK game.
IDisputeGame anchorGame = anchorStateRegistry.anchorGame();
assertEq(address(anchorGame), address(zkGameProxy));
}

/// @notice Tests that a valid ZK game with an older l2SequenceNumber cannot update the anchor.
function testFuzz_setAnchorState_olderValidGameClaim_fails(uint256 _l2SequenceNumber) public {
// First, set the anchor to the ZK game so we have a known anchor block.
_mockZkGameAsValid();
vm.prank(address(zkGameProxy));
anchorStateRegistry.setAnchorState(zkGameProxy);

(, uint256 anchorBlockNumber) = anchorStateRegistry.getAnchorRoot();

// Bound to at or below the anchor.
_l2SequenceNumber = bound(_l2SequenceNumber, 0, anchorBlockNumber);

// Mock the ZK game's sequence number to be at or below the anchor.
vm.mockCall(
address(zkGameProxy), abi.encodeCall(zkGameProxy.l2SequenceNumber, ()), abi.encode(_l2SequenceNumber)
);

// Capture state before the rejected call.
(Hash rootBefore, uint256 l2BlockNumberBefore) = anchorStateRegistry.getAnchorRoot();

vm.prank(address(zkGameProxy));
vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector);
anchorStateRegistry.setAnchorState(zkGameProxy);

// Confirm that the anchor state has not updated.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot();
assertEq(updatedL2BlockNumber, l2BlockNumberBefore);
assertEq(updatedRoot.raw(), rootBefore.raw());
}

/// @notice Tests that a blacklisted ZK game cannot update the anchor state.
function test_setAnchorState_blacklistedGame_fails() public {
_mockZkGameAsValid();

(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot();

// Blacklist the ZK game.
vm.prank(superchainConfig.guardian());
anchorStateRegistry.blacklistDisputeGame(zkGameProxy);

vm.prank(address(zkGameProxy));
vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector);
anchorStateRegistry.setAnchorState(zkGameProxy);

// Confirm that the anchor state has not updated.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot();
assertEq(updatedL2BlockNumber, l2BlockNumber);
assertEq(updatedRoot.raw(), root.raw());
}

/// @notice Tests that a retired ZK game cannot update the anchor state.
function test_setAnchorState_retiredGame_fails() public {
_mockZkGameAsValid();

(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot();

// Retire all games by setting retirement timestamp before game creation.
vm.prank(superchainConfig.guardian());
anchorStateRegistry.updateRetirementTimestamp();

vm.prank(address(zkGameProxy));
vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector);
anchorStateRegistry.setAnchorState(zkGameProxy);

// Confirm that the anchor state has not updated.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot();
assertEq(updatedL2BlockNumber, l2BlockNumber);
assertEq(updatedRoot.raw(), root.raw());
}

/// @notice Tests that a ZK game resolved as CHALLENGER_WINS cannot update the anchor state.
function test_setAnchorState_challengerWins_fails() public {
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot();

vm.mockCall(
address(zkGameProxy), abi.encodeCall(zkGameProxy.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)
);
vm.mockCall(
address(zkGameProxy), abi.encodeCall(zkGameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true)
);
vm.mockCall(address(zkGameProxy), abi.encodeCall(zkGameProxy.resolvedAt, ()), abi.encode(block.timestamp));
vm.mockCall(
address(zkGameProxy), abi.encodeCall(zkGameProxy.l2SequenceNumber, ()), abi.encode(zkL2SequenceNumber)
);
vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1);

vm.prank(address(zkGameProxy));
vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector);
anchorStateRegistry.setAnchorState(zkGameProxy);

// Confirm that the anchor state has not updated.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot();
assertEq(updatedL2BlockNumber, l2BlockNumber);
assertEq(updatedRoot.raw(), root.raw());
}

/// @notice Tests that an unfinalized ZK game cannot update the anchor state.
function testFuzz_setAnchorState_notFinalized_fails(uint256 _resolvedAtTimestamp) public {
uint256 finalityDelay = optimismPortal2.disputeGameFinalityDelaySeconds();
// Bound to avoid overflow when adding finalityDelay.
_resolvedAtTimestamp = bound(_resolvedAtTimestamp, block.timestamp, type(uint64).max);

(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot();

vm.mockCall(address(zkGameProxy), abi.encodeCall(zkGameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS));
vm.mockCall(
address(zkGameProxy), abi.encodeCall(zkGameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true)
);
vm.mockCall(address(zkGameProxy), abi.encodeCall(zkGameProxy.resolvedAt, ()), abi.encode(_resolvedAtTimestamp));
vm.mockCall(
address(zkGameProxy), abi.encodeCall(zkGameProxy.l2SequenceNumber, ()), abi.encode(zkL2SequenceNumber)
);
// Warp to before the finality delay has elapsed.
vm.warp(_resolvedAtTimestamp + finalityDelay);

vm.prank(address(zkGameProxy));
vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector);
anchorStateRegistry.setAnchorState(zkGameProxy);

// Confirm that the anchor state has not updated.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot();
assertEq(updatedL2BlockNumber, l2BlockNumber);
assertEq(updatedRoot.raw(), root.raw());
}

/// @notice Tests that a ZK game cannot update the anchor state when the superchain is paused.
function test_setAnchorState_superchainPaused_fails() public {
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot();

vm.prank(superchainConfig.guardian());
superchainConfig.pause(address(0));

vm.prank(address(zkGameProxy));
vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector);
anchorStateRegistry.setAnchorState(zkGameProxy);

// Confirm that the anchor state has not updated.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot();
assertEq(updatedL2BlockNumber, l2BlockNumber);
assertEq(updatedRoot.raw(), root.raw());
}
}

/// @title AnchorStateRegistry_IsGameClaimValid_ZkDisputeGame_Test
/// @notice Tests the `isGameClaimValid` function with ZKDisputeGame.
contract AnchorStateRegistry_IsGameClaimValid_ZkDisputeGame_Test is AnchorStateRegistry_ZkDisputeGame_TestInit {
/// @notice Tests that a valid ZK game claim is recognized.
function test_isGameClaimValid_validClaim_succeeds() public {
_mockZkGameAsValid();

assertTrue(anchorStateRegistry.isGameClaimValid(zkGameProxy));
}

/// @notice Tests that a blacklisted ZK game claim is not valid.
function test_isGameClaimValid_blacklisted_succeeds() public {
_mockZkGameAsValid();

vm.prank(superchainConfig.guardian());
anchorStateRegistry.blacklistDisputeGame(zkGameProxy);

assertFalse(anchorStateRegistry.isGameClaimValid(zkGameProxy));
}

/// @notice Tests that a retired ZK game claim is not valid.
function test_isGameClaimValid_retired_succeeds() public {
_mockZkGameAsValid();

// Retire all games by setting retirement timestamp before game creation.
vm.prank(superchainConfig.guardian());
anchorStateRegistry.updateRetirementTimestamp();

assertFalse(anchorStateRegistry.isGameClaimValid(zkGameProxy));
}

/// @notice Tests that a ZK game claim is not valid when it was not the respected type at creation.
function test_isGameClaimValid_notRespected_succeeds() public {
_mockZkGameAsValid();

// Mock that the game was not respected when created.
vm.mockCall(
address(zkGameProxy), abi.encodeCall(zkGameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(false)
);

assertFalse(anchorStateRegistry.isGameClaimValid(zkGameProxy));
}
}

/// @title AnchorStateRegistry_IsGameProper_ZkDisputeGame_Test
/// @notice Tests the `isGameProper` function with ZKDisputeGame.
contract AnchorStateRegistry_IsGameProper_ZkDisputeGame_Test is AnchorStateRegistry_ZkDisputeGame_TestInit {
/// @notice Tests that a ZK game meeting all conditions is proper.
function test_isGameProper_meetsAllConditions_succeeds() public view {
assertTrue(anchorStateRegistry.isGameProper(zkGameProxy));
}

/// @notice Tests that a blacklisted ZK game is not proper.
function test_isGameProper_blacklisted_succeeds() public {
vm.prank(superchainConfig.guardian());
anchorStateRegistry.blacklistDisputeGame(zkGameProxy);

assertFalse(anchorStateRegistry.isGameProper(zkGameProxy));
}

/// @notice Tests that an unregistered ZK game is not proper.
function test_isGameProper_notRegistered_succeeds() public {
// Mock the factory to report the ZK game as not registered.
vm.mockCall(
address(disputeGameFactory),
abi.encodeCall(
disputeGameFactory.games, (zkGameProxy.gameType(), zkGameProxy.rootClaim(), zkGameProxy.extraData())
),
abi.encode(address(0), 0)
);

assertFalse(anchorStateRegistry.isGameProper(zkGameProxy));
}

/// @notice Tests that a ZK game is not proper when the superchain is paused.
function test_isGameProper_superchainPaused_succeeds() public {
vm.prank(superchainConfig.guardian());
superchainConfig.pause(address(0));

assertFalse(anchorStateRegistry.isGameProper(zkGameProxy));
}

/// @notice Tests that a retired ZK game is not proper.
function test_isGameProper_retired_succeeds() public {
vm.prank(superchainConfig.guardian());
anchorStateRegistry.updateRetirementTimestamp();

assertFalse(anchorStateRegistry.isGameProper(zkGameProxy));
}
}

/// @title AnchorStateRegistry_BlacklistDisputeGame_ZkDisputeGame_Test
/// @notice Tests the `blacklistDisputeGame` function with ZKDisputeGame.
contract AnchorStateRegistry_BlacklistDisputeGame_ZkDisputeGame_Test is AnchorStateRegistry_ZkDisputeGame_TestInit {
/// @notice Tests that a ZK game can be blacklisted.
function test_blacklistDisputeGame_succeeds() public {
vm.prank(superchainConfig.guardian());
anchorStateRegistry.blacklistDisputeGame(zkGameProxy);

assertTrue(anchorStateRegistry.disputeGameBlacklist(zkGameProxy));
}
}
Loading
Loading