Commit 2cbbf069 authored by clabby's avatar clabby

Time-box `FaultDisputeGame::resolve()`

parent 08d44aea
......@@ -275,10 +275,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
/// @inheritdoc IDisputeGame
function resolve() external returns (GameStatus status_) {
// TODO: Do not allow resolution before clocks run out.
if (status != GameStatus.IN_PROGRESS) {
// If the game is not in progress, it cannot be resolved.
if (status != GameStatus.IN_PROGRESS) {
revert GameNotInProgress();
}
......@@ -298,7 +296,6 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
// If the claim is not a dangling node above the bottom of the tree,
// we can skip over it. These nodes are not relevant to the game resolution.
Position claimPos = claim.position;
if (claim.countered) {
continue;
}
......@@ -306,7 +303,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
// If the claim is a dangling node, we can check if it is the left-most
// dangling node we've come across so far. If it is, we can update the
// left-most trace index.
uint256 traceIndex = claimPos.traceIndex(MAX_GAME_DEPTH);
uint256 traceIndex = claim.position.traceIndex(MAX_GAME_DEPTH);
if (traceIndex < leftMostTraceIndex) {
leftMostTraceIndex = traceIndex;
unchecked {
......@@ -315,12 +312,29 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
}
}
// Create a reference to the left most uncontested claim and its parent.
ClaimData storage leftMostUncontested = claimData[leftMostIndex];
// If the left most uncontested claim's parent has not expired their clock, the game
// cannot be resolved. If the left most uncontested claim is the root, no nodes qualified,
// and we check if 3.5 days has passed since the root claim's creation.
uint256 parentIndex = leftMostUncontested.parentIndex;
Clock opposingClock = parentIndex == type(uint32).max
? leftMostUncontested.clock
: claimData[parentIndex].clock;
if (
Duration.unwrap(opposingClock.duration()) +
(block.timestamp - Timestamp.unwrap(opposingClock.timestamp())) <=
Duration.unwrap(GAME_DURATION) >> 1
) {
revert ClockNotExpired();
}
// If the left-most dangling node is at an even depth, the defender wins.
// Otherwise, the challenger wins and the root claim is deemed invalid.
if (
// slither-disable-next-line weak-prng
claimData[leftMostIndex].position.depth() % 2 == 0 &&
leftMostTraceIndex != type(uint128).max
leftMostUncontested.position.depth() % 2 == 0 && leftMostTraceIndex != type(uint128).max
) {
status_ = GameStatus.DEFENDER_WINS;
} else {
......
......@@ -39,6 +39,9 @@ error GameNotInProgress();
/// @notice Thrown when a move is attempted to be made after the clock has timed out.
error ClockTimeExceeded();
/// @notice Thrown when the game is attempted to be resolved too early.
error ClockNotExpired();
/// @notice Thrown when a move is attempted to be made at or greater than the max depth of the game.
error GameDepthExceeded();
......
......@@ -130,7 +130,7 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
}
/// @dev Tests that an attempt to defend the root claim reverts with the `CannotDefendRootClaim` error.
function test_defendRoot_invalidMove_reverts() public {
function test_move_defendRoot_reverts() public {
vm.expectRevert(CannotDefendRootClaim.selector);
gameProxy.defend(0, Claim.wrap(bytes32(uint256(5))));
}
......@@ -175,6 +175,49 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
gameProxy.attack(0, Claim.wrap(bytes32(uint256(5))));
}
/// @notice Static unit test for the correctness of the chess clock incrementation.
function test_move_clockCorrectness_succeeds() public {
(, , , , Clock clock) = gameProxy.claimData(0);
assertEq(
Clock.unwrap(clock),
Clock.unwrap(LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))))
);
Claim claim = Claim.wrap(bytes32(uint256(5)));
vm.warp(block.timestamp + 15);
gameProxy.attack(0, claim);
(, , , , clock) = gameProxy.claimData(1);
assertEq(
Clock.unwrap(clock),
Clock.unwrap(LibClock.wrap(Duration.wrap(15), Timestamp.wrap(uint64(block.timestamp))))
);
vm.warp(block.timestamp + 10);
gameProxy.attack(1, claim);
(, , , , clock) = gameProxy.claimData(2);
assertEq(
Clock.unwrap(clock),
Clock.unwrap(LibClock.wrap(Duration.wrap(10), Timestamp.wrap(uint64(block.timestamp))))
);
vm.warp(block.timestamp + 10);
gameProxy.attack(2, claim);
(, , , , clock) = gameProxy.claimData(3);
assertEq(
Clock.unwrap(clock),
Clock.unwrap(LibClock.wrap(Duration.wrap(25), Timestamp.wrap(uint64(block.timestamp))))
);
vm.warp(block.timestamp + 10);
gameProxy.attack(3, claim);
(, , , , clock) = gameProxy.claimData(4);
assertEq(
Clock.unwrap(clock),
Clock.unwrap(LibClock.wrap(Duration.wrap(20), Timestamp.wrap(uint64(block.timestamp))))
);
}
/// @dev Tests that an identical claim cannot be made twice. The duplicate claim attempt should
/// revert with the `ClaimAlreadyExists` error.
function test_move_duplicateClaim_reverts() public {
......@@ -189,7 +232,7 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
}
/// @dev Static unit test for the correctness of an opening attack.
function test_simpleAttack_succeeds() public {
function test_move_simpleAttack_succeeds() public {
// Warp ahead 5 seconds.
vm.warp(block.timestamp + 5);
......@@ -237,11 +280,19 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
/// @dev Static unit test for the correctness an uncontested root resolution.
function test_resolve_rootUncontested_succeeds() public {
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
GameStatus status = gameProxy.resolve();
assertEq(uint8(status), uint8(GameStatus.DEFENDER_WINS));
assertEq(uint8(gameProxy.status()), uint8(GameStatus.DEFENDER_WINS));
}
/// @dev Static unit test for the correctness an uncontested root resolution.
function test_resolve_rootUncontestedClockNotExpired_succeeds() public {
vm.warp(block.timestamp + 3 days + 12 hours);
vm.expectRevert(ClockNotExpired.selector);
gameProxy.resolve();
}
/// @dev Static unit test asserting that resolve reverts when the game state is
/// not in progress.
function test_resolve_notInProgress_reverts() public {
......@@ -263,6 +314,8 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
function test_resolve_rootContested_succeeds() public {
gameProxy.attack(0, Claim.wrap(bytes32(uint256(5))));
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
GameStatus status = gameProxy.resolve();
assertEq(uint8(status), uint8(GameStatus.CHALLENGER_WINS));
assertEq(uint8(gameProxy.status()), uint8(GameStatus.CHALLENGER_WINS));
......@@ -273,6 +326,8 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
gameProxy.attack(0, Claim.wrap(bytes32(uint256(5))));
gameProxy.defend(1, Claim.wrap(bytes32(uint256(6))));
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
GameStatus status = gameProxy.resolve();
assertEq(uint8(status), uint8(GameStatus.DEFENDER_WINS));
assertEq(uint8(gameProxy.status()), uint8(GameStatus.DEFENDER_WINS));
......@@ -285,6 +340,8 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
gameProxy.defend(1, Claim.wrap(bytes32(uint256(6))));
gameProxy.defend(1, Claim.wrap(bytes32(uint256(7))));
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
GameStatus status = gameProxy.resolve();
assertEq(uint8(status), uint8(GameStatus.CHALLENGER_WINS));
assertEq(uint8(gameProxy.status()), uint8(GameStatus.CHALLENGER_WINS));
......@@ -499,6 +556,9 @@ contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot is OneVsOne_Arena {
// Play the game until a step is forced.
challenger.play(0);
// Warp ahead to expire the other player's clock.
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
// Resolve the game and assert that the honest player challenged the root
// claim successfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS));
......@@ -517,6 +577,9 @@ contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot is OneVsOne_Arena {
// Play the game until a step is forced.
challenger.play(0);
// Warp ahead to expire the other player's clock.
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
// Resolve the game and assert that the dishonest player challenged the root
// claim unsuccessfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
......@@ -535,6 +598,9 @@ contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2 is OneVsOne_Arena {
// Play the game until a step is forced.
challenger.play(0);
// Warp ahead to expire the other player's clock.
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
// Resolve the game and assert that the honest player challenged the root
// claim successfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS));
......@@ -553,6 +619,9 @@ contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot2 is OneVsOne_Arena {
// Play the game until a step is forced.
challenger.play(0);
// Warp ahead to expire the other player's clock.
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
// Resolve the game and assert that the dishonest player challenged the root
// claim unsuccessfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
......@@ -571,6 +640,9 @@ contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3 is OneVsOne_Arena {
// Play the game until a step is forced.
challenger.play(0);
// Warp ahead to expire the other player's clock.
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
// Resolve the game and assert that the honest player challenged the root
// claim successfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS));
......@@ -589,6 +661,9 @@ contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot3 is OneVsOne_Arena {
// Play the game until a step is forced.
challenger.play(0);
// Warp ahead to expire the other player's clock.
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
// Resolve the game and assert that the dishonest player challenged the root
// claim unsuccessfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment