Skip to content

feat(DK): pass commit and appeal quicker #1955

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion contracts/src/arbitration/KlerosCoreBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,10 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable
dispute.period = Period.appeal;
emit AppealPossible(_disputeID, dispute.arbitrated);
} else if (dispute.period == Period.appeal) {
if (block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)]) {
if (
block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] &&
!disputeKits[round.disputeKitID].isAppealFunded(_disputeID)
) {
revert AppealPeriodNotPassed();
}
dispute.period = Period.execution;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -517,12 +517,32 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
}

/// @dev Returns true if all of the jurors have cast their votes for the last round.
/// Note that this function is to be called directly by the core contract and is not for off-chain usage.
/// @param _coreDisputeID The ID of the dispute in Kleros Core.
/// @return Whether all of the jurors have cast their votes for the last round.
function areVotesAllCast(uint256 _coreDisputeID) external view override returns (bool) {
Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]];
Round storage round = dispute.rounds[dispute.rounds.length - 1];
return round.totalVoted == round.votes.length;

(uint96 courtID, , , , ) = core.disputes(_coreDisputeID);
(, bool hiddenVotes, , , , , ) = core.courts(courtID);
uint256 expectedTotalVoted = hiddenVotes ? round.totalCommitted : round.votes.length;

return round.totalVoted == expectedTotalVoted;
}

/// @dev Returns true if the appeal funding is finished prematurely (e.g. when losing side didn't fund).
/// Note that this function is to be called directly by the core contract and is not for off-chain usage.
/// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit.
/// @return Whether the appeal funding is finished.
function isAppealFunded(uint256 _coreDisputeID) external view override returns (bool) {
(uint256 appealPeriodStart, uint256 appealPeriodEnd) = core.appealPeriod(_coreDisputeID);

uint256[] memory fundedChoices = getFundedChoices(_coreDisputeID);
// Uses block.timestamp from the current tx when called by the core contract.
return (fundedChoices.length == 0 &&
block.timestamp - appealPeriodStart >=
((appealPeriodEnd - appealPeriodStart) * LOSER_APPEAL_PERIOD_MULTIPLIER) / ONE_BASIS_POINT);
}

/// @dev Returns true if the specified voter was active in this round.
Expand Down
5 changes: 5 additions & 0 deletions contracts/src/arbitration/interfaces/IDisputeKit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ interface IDisputeKit {
/// @return Whether all of the jurors have cast their votes for the last round.
function areVotesAllCast(uint256 _coreDisputeID) external view returns (bool);

/// @dev Returns true if the appeal funding is finished prematurely (e.g. when losing side didn't fund).
/// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit.
/// @return Whether the appeal funding is finished.
function isAppealFunded(uint256 _coreDisputeID) external view returns (bool);

/// @dev Returns true if the specified voter was active in this round.
/// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit.
/// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit.
Expand Down
95 changes: 95 additions & 0 deletions contracts/test/foundry/KlerosCore.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1801,6 +1801,9 @@ contract KlerosCoreTest is Test {
assertEq(totalVoted, 2, "totalVoted should be 2");
assertEq(choiceCount, 1, "choiceCount should be 1 for first choice");

vm.expectRevert(KlerosCoreBase.VotePeriodNotPassed.selector);
core.passPeriod(disputeID);

voteIDs = new uint256[](1);
voteIDs[0] = 2; // Cast another vote to declare a new winner.

Expand Down Expand Up @@ -1874,6 +1877,64 @@ contract KlerosCoreTest is Test {
assertEq(overridden, false, "Not overridden");
}

function test_castVote_quickPassPeriod() public {
// Change hidden votes in general court
uint256 disputeID = 0;
vm.prank(governor);
core.changeCourtParameters(
GENERAL_COURT,
true, // Hidden votes
1000, // min stake
10000, // alpha
0.03 ether, // fee for juror
511, // jurors for jump
[uint256(60), uint256(120), uint256(180), uint256(240)] // Times per period
);

vm.prank(staker1);
core.setStake(GENERAL_COURT, 10000);
vm.prank(disputer);
arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action");
vm.warp(block.timestamp + minStakingTime);
sortitionModule.passPhase(); // Generating
vm.roll(block.number + rngLookahead + 1);
sortitionModule.passPhase(); // Drawing phase
core.draw(disputeID, DEFAULT_NB_OF_JURORS);

uint256 YES = 1;
uint256 salt = 123455678;
uint256[] memory voteIDs = new uint256[](1);
voteIDs[0] = 0;
bytes32 commit;

vm.warp(block.timestamp + timesPerPeriod[0]);
core.passPeriod(disputeID);

commit = keccak256(abi.encodePacked(YES, salt));

vm.prank(staker1);
disputeKit.castCommit(disputeID, voteIDs, commit);

(, , , uint256 totalCommited, uint256 nbVoters, uint256 choiceCount) = disputeKit.getRoundInfo(disputeID, 0, 0);
assertEq(totalCommited, 1, "totalCommited should be 1");
assertEq(disputeKit.areCommitsAllCast(disputeID), false, "Commits should not all be cast");

vm.warp(block.timestamp + timesPerPeriod[1]);
core.passPeriod(disputeID);

vm.prank(staker1);
disputeKit.castVote(disputeID, voteIDs, YES, salt, "XYZ");

(, , uint256 totalVoted, , , ) = disputeKit.getRoundInfo(disputeID, 0, 0);
assertEq(totalVoted, 1, "totalVoted should be 1");
assertEq(disputeKit.areVotesAllCast(disputeID), true, "Every committed vote was cast");

// Should pass period by counting only committed votes.
vm.expectEmit(true, true, true, true);
emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.appeal);
core.passPeriod(disputeID);
}

function test_appeal_fundOneSide() public {
uint256 disputeID = 0;
vm.deal(address(disputeKit), 1 ether);
Expand Down Expand Up @@ -2184,6 +2245,40 @@ contract KlerosCoreTest is Test {
assertEq(account, staker1, "Wrong drawn account in the classic DK");
}

function test_appeal_quickPassPeriod() public {
uint256 disputeID = 0;

vm.prank(staker1);
core.setStake(GENERAL_COURT, 10000);
vm.prank(disputer);
arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action");
vm.warp(block.timestamp + minStakingTime);
sortitionModule.passPhase(); // Generating
vm.roll(block.number + rngLookahead + 1);
sortitionModule.passPhase(); // Drawing phase

core.draw(disputeID, DEFAULT_NB_OF_JURORS);
vm.warp(block.timestamp + timesPerPeriod[0]);
core.passPeriod(disputeID); // Vote

uint256[] memory voteIDs = new uint256[](3);
voteIDs[0] = 0;
voteIDs[1] = 1;
voteIDs[2] = 2;

vm.prank(staker1);
disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ");

core.passPeriod(disputeID); // Appeal

vm.warp(block.timestamp + timesPerPeriod[3] / 2);

// Should pass to execution period without waiting for the 2nd half of the appeal.
vm.expectEmit(true, true, true, true);
emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.execution);
core.passPeriod(disputeID);
}

function test_execute() public {
uint256 disputeID = 0;

Expand Down
Loading