diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 661a9df23..743f052d0 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -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; diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 3b06a1891..ff6291e58 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -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. diff --git a/contracts/src/arbitration/interfaces/IDisputeKit.sol b/contracts/src/arbitration/interfaces/IDisputeKit.sol index f0b3d4d90..3944009b8 100644 --- a/contracts/src/arbitration/interfaces/IDisputeKit.sol +++ b/contracts/src/arbitration/interfaces/IDisputeKit.sol @@ -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. diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index b974f6d9c..291b969d0 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -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. @@ -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); @@ -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;