diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 698a9a547..859b3b0f5 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -24,7 +24,6 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi struct Dispute { Round[] rounds; // Rounds of the dispute. 0 is the default round, and [1, ..n] are the appeal rounds. uint256 numberOfChoices; // The number of choices jurors have when voting. This does not include choice `0` which is reserved for "refuse to arbitrate". - bool jumped; // True if dispute jumped to a parent dispute kit and won't be handled by this DK anymore. mapping(uint256 => uint256) coreRoundIDToLocal; // Maps id of the round in the core contract to the index of the round of related local dispute. bytes extraData; // Extradata for the dispute. uint256[10] __gap; // Reserved slots for future upgrades. @@ -54,6 +53,11 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi uint256[10] __gap; // Reserved slots for future upgrades. } + struct Active { + bool dispute; // True if at least one round in the dispute has been active on this Dispute Kit. False if the dispute is unknown to this Dispute Kit. + bool currentRound; // True if the dispute's current round is active on this Dispute Kit. False if the dispute has jumped to another Dispute Kit. + } + // ************************************* // // * Storage * // // ************************************* // @@ -67,7 +71,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi Dispute[] public disputes; // Array of the locally created disputes. mapping(uint256 => uint256) public coreDisputeIDToLocal; // Maps the dispute ID in Kleros Core to the local dispute ID. bool public singleDrawPerJuror; // Whether each juror can only draw once per dispute, false by default. - mapping(uint256 coreDisputeID => bool) public coreDisputeIDToActive; // True if this dispute kit is active for this core dispute ID. + mapping(uint256 coreDisputeID => Active) public coreDisputeIDToActive; // Active status of the dispute and the current round. address public wNative; // The wrapped native token for safeSend(). uint256 public jumpDisputeKitID; // The ID of the dispute kit in Kleros Core disputeKits array that the dispute should switch to after the court jump, in case the new court doesn't support this dispute kit. @@ -106,17 +110,10 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// @notice To be emitted when the contributed funds are withdrawn. /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. - /// @param _coreRoundID The identifier of the round in the Arbitrator contract. /// @param _choice The choice that is being funded. /// @param _contributor The address of the contributor. /// @param _amount The amount withdrawn. - event Withdrawal( - uint256 indexed _coreDisputeID, - uint256 indexed _coreRoundID, - uint256 _choice, - address indexed _contributor, - uint256 _amount - ); + event Withdrawal(uint256 indexed _coreDisputeID, uint256 _choice, address indexed _contributor, uint256 _amount); /// @notice To be emitted when a choice is fully funded for an appeal. /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. @@ -138,8 +135,9 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi _; } - modifier notJumped(uint256 _coreDisputeID) { - if (disputes[coreDisputeIDToLocal[_coreDisputeID]].jumped) revert DisputeJumpedToParentDK(); + modifier isActive(uint256 _coreDisputeID) { + if (!coreDisputeIDToActive[_coreDisputeID].dispute) revert DisputeUnknownInThisDisputeKit(); + if (!coreDisputeIDToActive[_coreDisputeID].currentRound) revert DisputeJumpedToAnotherDisputeKit(); _; } @@ -206,20 +204,29 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi bytes calldata _extraData, uint256 /*_nbVotes*/ ) external override onlyByCore { - uint256 localDisputeID = disputes.length; - Dispute storage dispute = disputes.push(); + uint256 localDisputeID; + Dispute storage dispute; + Active storage active = coreDisputeIDToActive[_coreDisputeID]; + if (active.dispute) { + // The dispute has already been created in this DK in a previous round. E.g. if DK1 jumps to DK2 and then back to DK1. + localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; + dispute = disputes[localDisputeID]; + } else { + // The dispute has not been created in this DK yet. + localDisputeID = disputes.length; + dispute = disputes.push(); + coreDisputeIDToLocal[_coreDisputeID] = localDisputeID; + } + + active.dispute = true; + active.currentRound = true; dispute.numberOfChoices = _numberOfChoices; dispute.extraData = _extraData; - dispute.jumped = false; // Possibly true if this DK has jumped in a previous round. - // New round in the Core should be created before the dispute creation in DK. + // KlerosCore.Round must have been already created. dispute.coreRoundIDToLocal[core.getNumberOfRounds(_coreDisputeID) - 1] = dispute.rounds.length; + dispute.rounds.push().tied = true; - Round storage round = dispute.rounds.push(); - round.tied = true; - - coreDisputeIDToLocal[_coreDisputeID] = localDisputeID; - coreDisputeIDToActive[_coreDisputeID] = true; emit DisputeCreation(_coreDisputeID, _numberOfChoices, _extraData); } @@ -227,7 +234,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi function draw( uint256 _coreDisputeID, uint256 _nonce - ) external override onlyByCore notJumped(_coreDisputeID) returns (address drawnAddress, uint96 fromSubcourtID) { + ) external override onlyByCore isActive(_coreDisputeID) returns (address drawnAddress, uint96 fromSubcourtID) { uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; Dispute storage dispute = disputes[localDisputeID]; uint256 localRoundID = dispute.rounds.length - 1; @@ -266,11 +273,10 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi uint256 _coreDisputeID, uint256[] calldata _voteIDs, bytes32 _commit - ) internal notJumped(_coreDisputeID) { + ) internal isActive(_coreDisputeID) { (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); if (period != KlerosCore.Period.commit) revert NotCommitPeriod(); if (_commit == bytes32(0)) revert EmptyCommit(); - if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID(); Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; Round storage round = dispute.rounds[dispute.rounds.length - 1]; @@ -313,11 +319,10 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi uint256 _salt, string memory _justification, address _juror - ) internal notJumped(_coreDisputeID) { + ) internal isActive(_coreDisputeID) { (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); if (period != KlerosCore.Period.vote) revert NotVotePeriod(); if (_voteIDs.length == 0) revert EmptyVoteIDs(); - if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID(); uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; Dispute storage dispute = disputes[localDisputeID]; @@ -364,10 +369,9 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// Note that the surplus deposit will be reimbursed. /// @param _coreDisputeID Index of the dispute in Kleros Core. /// @param _choice A choice that receives funding. - function fundAppeal(uint256 _coreDisputeID, uint256 _choice) external payable notJumped(_coreDisputeID) { + function fundAppeal(uint256 _coreDisputeID, uint256 _choice) external payable isActive(_coreDisputeID) { Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; if (_choice > dispute.numberOfChoices) revert ChoiceOutOfBounds(); - if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID(); (uint256 appealPeriodStart, uint256 appealPeriodEnd) = core.appealPeriod(_coreDisputeID); if (block.timestamp < appealPeriodStart || block.timestamp >= appealPeriodEnd) revert NotAppealPeriod(); @@ -417,7 +421,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi if (core.isDisputeKitJumping(_coreDisputeID)) { // Don't create a new round in case of a jump, and remove local dispute from the flow. - dispute.jumped = true; + coreDisputeIDToActive[_coreDisputeID].currentRound = false; } else { // Don't subtract 1 from length since both round arrays haven't been updated yet. dispute.coreRoundIDToLocal[coreRoundID + 1] = dispute.rounds.length; @@ -433,48 +437,50 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// @notice Allows those contributors who attempted to fund an appeal round to withdraw any reimbursable fees or rewards after the dispute gets resolved. /// @dev Withdrawals are not possible if the core contract is paused. + /// @dev It can be called after the dispute has jumped to another dispute kit. /// @param _coreDisputeID Index of the dispute in Kleros Core contract. /// @param _beneficiary The address whose rewards to withdraw. - /// @param _coreRoundID The round in the Kleros Core contract the caller wants to withdraw from. /// @param _choice The ruling option that the caller wants to withdraw from. /// @return amount The withdrawn amount. function withdrawFeesAndRewards( uint256 _coreDisputeID, address payable _beneficiary, - uint256 _coreRoundID, uint256 _choice ) external returns (uint256 amount) { (, , , bool isRuled, ) = core.disputes(_coreDisputeID); if (!isRuled) revert DisputeNotResolved(); if (core.paused()) revert CoreIsPaused(); - if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID(); + if (!coreDisputeIDToActive[_coreDisputeID].dispute) revert DisputeUnknownInThisDisputeKit(); Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; (uint256 finalRuling, , ) = core.currentRuling(_coreDisputeID); - if (!round.hasPaid[_choice]) { - // Allow to reimburse if funding was unsuccessful for this ruling option. - amount = round.contributions[_beneficiary][_choice]; - } else { - // Funding was successful for this ruling option. - if (_choice == finalRuling) { - // This ruling option is the ultimate winner. - amount = round.paidFees[_choice] > 0 - ? (round.contributions[_beneficiary][_choice] * round.feeRewards) / round.paidFees[_choice] - : 0; - } else if (!round.hasPaid[finalRuling]) { - // The ultimate winner was not funded in this round. In this case funded ruling option(s) are reimbursed. - amount = - (round.contributions[_beneficiary][_choice] * round.feeRewards) / - (round.paidFees[round.fundedChoices[0]] + round.paidFees[round.fundedChoices[1]]); + for (uint256 i = 0; i < dispute.rounds.length; i++) { + Round storage round = dispute.rounds[i]; + + if (!round.hasPaid[_choice]) { + // Allow to reimburse if funding was unsuccessful for this ruling option. + amount += round.contributions[_beneficiary][_choice]; + } else { + // Funding was successful for this ruling option. + if (_choice == finalRuling) { + // This ruling option is the ultimate winner. + amount += round.paidFees[_choice] > 0 + ? (round.contributions[_beneficiary][_choice] * round.feeRewards) / round.paidFees[_choice] + : 0; + } else if (!round.hasPaid[finalRuling]) { + // The ultimate winner was not funded in this round. In this case funded ruling option(s) are reimbursed. + amount += + (round.contributions[_beneficiary][_choice] * round.feeRewards) / + (round.paidFees[round.fundedChoices[0]] + round.paidFees[round.fundedChoices[1]]); + } } + round.contributions[_beneficiary][_choice] = 0; } - round.contributions[_beneficiary][_choice] = 0; if (amount != 0) { _beneficiary.safeSend(amount, wNative); - emit Withdrawal(_coreDisputeID, _coreRoundID, _choice, _beneficiary, amount); + emit Withdrawal(_coreDisputeID, _choice, _beneficiary, amount); } } @@ -748,11 +754,11 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi error OwnerOnly(); error KlerosCoreOnly(); - error DisputeJumpedToParentDK(); + error DisputeJumpedToAnotherDisputeKit(); + error DisputeUnknownInThisDisputeKit(); error UnsuccessfulCall(); error NotCommitPeriod(); error EmptyCommit(); - error NotActiveForCoreDisputeID(); error JurorHasToOwnTheVote(); error NotVotePeriod(); error EmptyVoteIDs(); diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol index dbcde618c..a4ec2f8dc 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol @@ -118,7 +118,7 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase { bytes32 _recoveryCommit, bytes32 _identity, bytes calldata _encryptedVote - ) external notJumped(_coreDisputeID) { + ) external { if (_recoveryCommit == bytes32(0)) revert EmptyRecoveryCommit(); uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; @@ -128,7 +128,7 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase { recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit; } - // `_castCommit()` ensures that the caller owns the vote + // `_castCommit()` ensures that the caller owns the vote and that dispute is active _castCommit(_coreDisputeID, _voteIDs, _commit); emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _recoveryCommit, _identity, _encryptedVote); } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol b/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol index 0ea8bdb41..fe2c0eb7b 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol @@ -102,7 +102,7 @@ contract DisputeKitShutter is DisputeKitClassicBase { bytes32 _recoveryCommit, bytes32 _identity, bytes calldata _encryptedVote - ) external notJumped(_coreDisputeID) { + ) external { if (_recoveryCommit == bytes32(0)) revert EmptyRecoveryCommit(); uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; @@ -112,7 +112,7 @@ contract DisputeKitShutter is DisputeKitClassicBase { recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit; } - // `_castCommit()` ensures that the caller owns the vote + // `_castCommit()` ensures that the caller owns the vote and that dispute is active _castCommit(_coreDisputeID, _voteIDs, _commit); emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _recoveryCommit, _identity, _encryptedVote); } diff --git a/contracts/src/arbitration/interfaces/IDisputeKit.sol b/contracts/src/arbitration/interfaces/IDisputeKit.sol index 20e42a0d2..88467d83f 100644 --- a/contracts/src/arbitration/interfaces/IDisputeKit.sol +++ b/contracts/src/arbitration/interfaces/IDisputeKit.sol @@ -32,6 +32,7 @@ interface IDisputeKit { /// @notice Creates a local dispute and maps it to the dispute ID in the Core contract. /// @dev Access restricted to Kleros Core only. + /// @dev The new `KlerosCore.Round` must be created before calling this function. /// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit. /// @param _numberOfChoices Number of choices of the dispute /// @param _extraData Additional info about the dispute, for possible use in future dispute kits. diff --git a/contracts/test/foundry/KlerosCore_Appeals.t.sol b/contracts/test/foundry/KlerosCore_Appeals.t.sol index 9a88bb1b2..c14ccfb43 100644 --- a/contracts/test/foundry/KlerosCore_Appeals.t.sol +++ b/contracts/test/foundry/KlerosCore_Appeals.t.sol @@ -298,8 +298,8 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { vm.prank(crowdfunder2); newDisputeKit.fundAppeal{value: 0.42 ether}(disputeID, 2); - (, bool jumped, ) = newDisputeKit.disputes(disputeID); - assertEq(jumped, true, "jumped should be true"); + (, bool currentRound) = newDisputeKit.coreDisputeIDToActive(disputeID); + assertEq(currentRound, false, "round should be jumped"); assertEq( (newDisputeKit.getFundedChoices(disputeID)).length, 2, @@ -312,12 +312,12 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { (uint96 courtID, , , , ) = core.disputes(disputeID); assertEq(courtID, GENERAL_COURT, "Wrong court ID"); - (, jumped, ) = disputeKit.disputes(disputeID); - assertEq(jumped, false, "jumped should be false in the DK that dispute jumped to"); + (, currentRound) = disputeKit.coreDisputeIDToActive(disputeID); + assertEq(currentRound, true, "round should be active in the DK that dispute jumped to"); // Check jump modifier vm.prank(address(core)); - vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToParentDK.selector); + vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToAnotherDisputeKit.selector); newDisputeKit.draw(disputeID, 1); // And check that draw in the new round works @@ -436,8 +436,8 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { vm.prank(crowdfunder2); disputeKit3.fundAppeal{value: 0.42 ether}(disputeID, 2); - (, bool jumped, ) = disputeKit3.disputes(disputeID); - assertEq(jumped, true, "jumped should be true"); + (, bool currentRound) = disputeKit3.coreDisputeIDToActive(disputeID); + assertEq(currentRound, false, "round should be jumped"); assertEq( (disputeKit3.getFundedChoices(disputeID)).length, 2, @@ -450,12 +450,12 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { (uint96 courtID, , , , ) = core.disputes(disputeID); assertEq(courtID, GENERAL_COURT, "Wrong court ID"); - (, jumped, ) = disputeKit2.disputes(disputeID); - assertEq(jumped, false, "jumped should be false in the DK that dispute jumped to"); + (, currentRound) = disputeKit2.coreDisputeIDToActive(disputeID); + assertEq(currentRound, true, "round should be active in the DK that dispute jumped to"); // Check jump modifier vm.prank(address(core)); - vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToParentDK.selector); + vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToAnotherDisputeKit.selector); disputeKit3.draw(disputeID, 1); // And check that draw in the new round works @@ -467,6 +467,313 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { assertEq(account, staker1, "Wrong drawn account in the classic DK"); } + function test_appeal_recurringDK() public { + // Test the behaviour when dispute jumps from DK3 to DK2 and then back to DK3 again. + + // Setup: create 2 more courts to facilitate appeal jump. Create 2 more DK. + // Set General Court as parent to court2, and court2 as parent to court3. dk2 as jump DK for dk3, and dk3 as jump DK for dk2. + // Ensure DK2 is supported by Court2 and DK3 is supported by court3. + // Preemptively add DK3 support for General court. + + // Initial dispute starts with Court3, DK3. + // Jumps to Court2, DK2. + // Then jumps to General Court, DK3. + uint256 disputeID = 0; + + uint96 courtID2 = 2; + uint96 courtID3 = 3; + + uint256 dkID2 = 2; + uint256 dkID3 = 3; + + DisputeKitClassic dkLogic = new DisputeKitClassic(); + + bytes memory initDataDk2 = abi.encodeWithSignature( + "initialize(address,address,address,uint256)", + owner, + address(core), + address(wNative), + dkID3 + ); + UUPSProxy proxyDk2 = new UUPSProxy(address(dkLogic), initDataDk2); + DisputeKitClassic disputeKit2 = DisputeKitClassic(address(proxyDk2)); + + bytes memory initDataDk3 = abi.encodeWithSignature( + "initialize(address,address,address,uint256)", + owner, + address(core), + address(wNative), + dkID2 + ); + UUPSProxy proxyDk3 = new UUPSProxy(address(dkLogic), initDataDk3); + DisputeKitClassic disputeKit3 = DisputeKitClassic(address(proxyDk3)); + + vm.prank(owner); + core.addNewDisputeKit(disputeKit2); + vm.prank(owner); + core.addNewDisputeKit(disputeKit3); + + uint256[] memory supportedDK = new uint256[](2); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + supportedDK[1] = dkID2; + vm.prank(owner); + core.createCourt( + GENERAL_COURT, + hiddenVotes, + minStake, + alpha, + feeForJuror, + 7, // jurors for jump. Minimal number to ensure jump after the first appeal + [uint256(60), uint256(120), uint256(180), uint256(240)], // Times per period + sortitionExtraData, + supportedDK + ); + assertEq(core.isSupported(courtID2, dkID2), true, "dkID2 should be supported by Court2"); + + (uint96 courtParent, , , , , uint256 courtJurorsForCourtJump) = core.courts(courtID2); + assertEq(courtParent, GENERAL_COURT, "Wrong court parent for court2"); + assertEq(courtJurorsForCourtJump, 7, "Wrong jurors for jump value for court2"); + + supportedDK = new uint256[](2); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + supportedDK[1] = dkID3; + vm.prank(owner); + core.createCourt( + courtID2, + hiddenVotes, + minStake, + alpha, + feeForJuror, + 3, // jurors for jump. Minimal number to ensure jump after the first appeal + [uint256(60), uint256(120), uint256(180), uint256(240)], // Times per period + sortitionExtraData, + supportedDK + ); + assertEq(core.isSupported(courtID3, dkID3), true, "dkID3 should be supported by Court3"); + + (courtParent, , , , , courtJurorsForCourtJump) = core.courts(courtID3); + assertEq(courtParent, courtID2, "Wrong court parent for court3"); + assertEq(courtJurorsForCourtJump, 3, "Wrong jurors for jump value for court3"); + + vm.prank(owner); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + supportedDK[1] = dkID3; + core.enableDisputeKits(GENERAL_COURT, supportedDK, true); + assertEq(core.isSupported(GENERAL_COURT, dkID3), true, "dkID3 should be supported by GENERAL_COURT"); + + bytes memory newExtraData = abi.encodePacked(uint256(courtID3), DEFAULT_NB_OF_JURORS, dkID3); + arbitrable.changeArbitratorExtraData(newExtraData); + + vm.prank(staker1); + core.setStake(courtID3, 20000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + // Round1 // + + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.disputeKitID, dkID3, "Wrong DK ID"); + + assertEq(disputeKit3.coreDisputeIDToLocal(disputeID), 0, "Wrong local dispute ID to core dispute ID"); + assertEq(disputeKit3.getNumberOfRounds(0), 1, "Wrong number of rounds dk3"); // local dispute id + (, uint256 localRoundID) = disputeKit3.getLocalDisputeRoundID(disputeID, 0); + assertEq(localRoundID, 0, "Wrong local round ID dk3"); + + (bool disputeActive, bool currentRound) = disputeKit3.coreDisputeIDToActive(0); + assertEq(disputeActive, true, "dispute should be active for dk3"); + assertEq(currentRound, true, "round should be active in dk3"); + + 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); + disputeKit3.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + core.passPeriod(disputeID); // Appeal + + vm.prank(crowdfunder1); + disputeKit3.fundAppeal{value: 0.63 ether}(disputeID, 1); + + assertEq(core.isDisputeKitJumping(disputeID), true, "Should be jumping"); + + vm.expectEmit(true, true, true, true); + emit KlerosCore.CourtJump(disputeID, 1, courtID3, courtID2); + vm.expectEmit(true, true, true, true); + emit KlerosCore.DisputeKitJump(disputeID, 1, dkID3, dkID2); + vm.expectEmit(true, true, true, true); + emit DisputeKitClassicBase.DisputeCreation(disputeID, 2, newExtraData); + vm.expectEmit(true, true, true, true); + emit KlerosCore.AppealDecision(disputeID, arbitrable); + vm.expectEmit(true, true, true, true); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.evidence); + vm.prank(crowdfunder2); + disputeKit3.fundAppeal{value: 0.42 ether}(disputeID, 2); + + // Round2 // + + (disputeActive, currentRound) = disputeKit3.coreDisputeIDToActive(0); + assertEq(disputeActive, true, "dispute should still be active for dk3"); + assertEq(currentRound, false, "round should be jumped in dk3"); + assertEq( + (disputeKit3.getFundedChoices(disputeID)).length, + 2, + "No fresh round created so the number of funded choices should be 2" + ); + assertEq(disputeKit3.coreDisputeIDToLocal(disputeID), 0, "core to local ID should not change for dk3"); + assertEq(disputeKit3.getNumberOfRounds(0), 1, "Wrong number of rounds dk3"); // local dispute id + (, localRoundID) = disputeKit3.getLocalDisputeRoundID(disputeID, 0); + assertEq(localRoundID, 0, "Local round ID should not change dk3"); + + round = core.getRoundInfo(disputeID, 1); + assertEq(round.disputeKitID, dkID2, "Wrong DK ID"); + assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count"); + (uint96 courtID, , , , ) = core.disputes(disputeID); + assertEq(courtID, courtID2, "Wrong court ID after jump"); + + (disputeActive, currentRound) = disputeKit2.coreDisputeIDToActive(0); + assertEq(disputeActive, true, "dispute should be active for dk2"); + assertEq(currentRound, true, "round should be active in the DK that dispute jumped to"); + assertEq(disputeKit2.coreDisputeIDToLocal(disputeID), 0, "Wrong local dispute ID to core dispute ID dk2"); + assertEq(disputeKit2.getNumberOfRounds(0), 1, "Wrong number of rounds dk2"); // local dispute id + (, localRoundID) = disputeKit2.getLocalDisputeRoundID(disputeID, 1); + assertEq(localRoundID, 0, "Wrong local round ID for dk2"); + + vm.prank(address(core)); + vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToAnotherDisputeKit.selector); + disputeKit3.draw(disputeID, 1); + + core.draw(disputeID, 7); // New round requires 7 jurors + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + voteIDs = new uint256[](7); + for (uint256 i = 0; i < voteIDs.length; i++) { + voteIDs[i] = i; + } + + vm.prank(staker1); + vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToAnotherDisputeKit.selector); + disputeKit3.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + vm.prank(staker1); + disputeKit2.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + core.passPeriod(disputeID); // Appeal + + vm.prank(crowdfunder1); + vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToAnotherDisputeKit.selector); + disputeKit3.fundAppeal{value: 1.35 ether}(disputeID, 1); + + assertEq(core.isDisputeKitJumping(disputeID), true, "Should be jumping"); + + vm.prank(crowdfunder1); + // appealCost is 0.45. (0.03 * 15) + disputeKit2.fundAppeal{value: 1.35 ether}(disputeID, 1); // 0.45 + (0.45 * 20000/10000). + + vm.expectEmit(true, true, true, true); + emit KlerosCore.CourtJump(disputeID, 2, courtID2, GENERAL_COURT); + vm.expectEmit(true, true, true, true); + emit KlerosCore.DisputeKitJump(disputeID, 2, dkID2, dkID3); + vm.prank(crowdfunder2); + disputeKit2.fundAppeal{value: 0.9 ether}(disputeID, 2); // 0.45 + (0.45 * 10000/10000). + + // Round3 // + + (disputeActive, currentRound) = disputeKit2.coreDisputeIDToActive(0); + assertEq(disputeActive, true, "dispute should still be active for dk2"); + assertEq(currentRound, false, "round should be jumped in dk2"); + assertEq( + (disputeKit2.getFundedChoices(disputeID)).length, + 2, + "No fresh round created so the number of funded choices should be 2 for dk2" + ); + assertEq( + disputeKit3.getFundedChoices(disputeID).length, + 0, + "Should be 0 funded choices in dk3 because fresh round" + ); + assertEq(disputeKit3.coreDisputeIDToLocal(disputeID), 0, "core to local ID should stay the same for dk3"); + assertEq(disputeKit3.getNumberOfRounds(0), 2, "Wrong number of rounds dk3 round3"); // local dispute id + (, localRoundID) = disputeKit3.getLocalDisputeRoundID(disputeID, 2); + assertEq(localRoundID, 1, "Wrong local round id for dk3 round3"); + + round = core.getRoundInfo(disputeID, 2); + assertEq(round.disputeKitID, dkID3, "Wrong DK ID"); + assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count"); + (courtID, , , , ) = core.disputes(disputeID); + assertEq(courtID, GENERAL_COURT, "Wrong court ID after jump"); + + (disputeActive, currentRound) = disputeKit3.coreDisputeIDToActive(0); // local dispute id + assertEq(disputeActive, true, "dispute should still be active for dk3"); + assertEq(currentRound, true, "round should be active in the DK that dispute jumped to"); + + assertEq( + disputeKit2.coreDisputeIDToLocal(disputeID), + 0, + "Wrong local dispute ID to core dispute ID dk2 round3" + ); + assertEq(disputeKit2.getNumberOfRounds(0), 1, "Wrong number of rounds dk2 round3"); // local dispute id + (, localRoundID) = disputeKit2.getLocalDisputeRoundID(disputeID, 1); + assertEq(localRoundID, 0, "Wrong local round ID for dk2 round3"); + + vm.prank(address(core)); + vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToAnotherDisputeKit.selector); + disputeKit2.draw(disputeID, 1); + + core.draw(disputeID, 15); // New round requires 15 jurors + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + voteIDs = new uint256[](15); + for (uint256 i = 0; i < voteIDs.length; i++) { + voteIDs[i] = i; + } + + vm.prank(staker1); + vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToAnotherDisputeKit.selector); + disputeKit2.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + vm.prank(staker1); + disputeKit3.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + vm.warp(block.timestamp + timesPerPeriod[2]); + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + core.executeRuling(disputeID); // winning choice is 2 + + // Appeal Rewards // + disputeKit3.withdrawFeesAndRewards(disputeID, payable(crowdfunder1), 1); // wrong side, no reward + + vm.expectEmit(); + emit DisputeKitClassicBase.Withdrawal(disputeID, 2, payable(crowdfunder2), 0.84 ether); + disputeKit3.withdrawFeesAndRewards(disputeID, payable(crowdfunder2), 2); // REWARDS + + disputeKit2.withdrawFeesAndRewards(disputeID, payable(crowdfunder1), 1); // wrong DK, no reward + + vm.expectEmit(); + emit DisputeKitClassicBase.Withdrawal(disputeID, 2, payable(crowdfunder2), 1.8 ether); + disputeKit2.withdrawFeesAndRewards(disputeID, payable(crowdfunder2), 2); // REWARDS + + vm.expectRevert(DisputeKitClassicBase.DisputeUnknownInThisDisputeKit.selector); + disputeKit.withdrawFeesAndRewards(disputeID, payable(crowdfunder1), 1); // wrong DK, no reward + + vm.expectRevert(DisputeKitClassicBase.DisputeUnknownInThisDisputeKit.selector); + disputeKit.withdrawFeesAndRewards(disputeID, payable(crowdfunder2), 2); // wrong DK, no reward + } + function test_appeal_quickPassPeriod() public { uint256 disputeID = 0; @@ -516,7 +823,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { vm.prank(disputer); arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - (uint256 numberOfChoices, , ) = disputeKit.disputes(disputeID); + (uint256 numberOfChoices, ) = disputeKit.disputes(disputeID); assertEq(numberOfChoices, numberOfOptions, "Wrong numberOfChoices"); diff --git a/contracts/test/foundry/KlerosCore_Disputes.t.sol b/contracts/test/foundry/KlerosCore_Disputes.t.sol index 2d029ddcf..93c4c0757 100644 --- a/contracts/test/foundry/KlerosCore_Disputes.t.sol +++ b/contracts/test/foundry/KlerosCore_Disputes.t.sol @@ -88,13 +88,15 @@ contract KlerosCore_DisputesTest is KlerosCore_TestBase { assertEq(address(round.feeToken), address(0), "feeToken should be 0"); assertEq(round.drawIterations, 0, "drawIterations should be 0"); - (uint256 numberOfChoices, bool jumped, bytes memory extraData) = disputeKit.disputes(disputeID); - + (uint256 numberOfChoices, bytes memory extraData) = disputeKit.disputes(disputeID); assertEq(numberOfChoices, 2, "Wrong numberOfChoices"); - assertEq(jumped, false, "jumped should be false"); assertEq(extraData, newExtraData, "Wrong extra data"); + + (bool dispute, bool currentRound) = disputeKit.coreDisputeIDToActive(0); + assertEq(dispute, true, "Dispute should be active in this DK"); + assertEq(currentRound, true, "Current round should be active in this DK"); + assertEq(disputeKit.coreDisputeIDToLocal(0), disputeID, "Wrong local disputeID"); - assertEq(disputeKit.coreDisputeIDToActive(0), true, "Dispute should be active in this DK"); ( uint256 winningChoice, diff --git a/contracts/test/foundry/KlerosCore_Execution.t.sol b/contracts/test/foundry/KlerosCore_Execution.t.sol index bbce3234a..81dd723a0 100644 --- a/contracts/test/foundry/KlerosCore_Execution.t.sol +++ b/contracts/test/foundry/KlerosCore_Execution.t.sol @@ -721,14 +721,14 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { core.passPeriod(disputeID); // Execution vm.expectRevert(DisputeKitClassicBase.DisputeNotResolved.selector); - disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 0, 1); + disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 1); core.executeRuling(disputeID); vm.prank(owner); core.pause(); vm.expectRevert(DisputeKitClassicBase.CoreIsPaused.selector); - disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 0, 1); + disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 1); vm.prank(owner); core.unpause(); @@ -737,12 +737,12 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(address(disputeKit).balance, 1.04 ether, "Wrong balance of the DK"); vm.expectEmit(true, true, true, true); - emit DisputeKitClassicBase.Withdrawal(disputeID, 0, 1, crowdfunder1, 0.63 ether); - disputeKit.withdrawFeesAndRewards(disputeID, payable(crowdfunder1), 0, 1); + emit DisputeKitClassicBase.Withdrawal(disputeID, 1, crowdfunder1, 0.63 ether); + disputeKit.withdrawFeesAndRewards(disputeID, payable(crowdfunder1), 1); vm.expectEmit(true, true, true, true); - emit DisputeKitClassicBase.Withdrawal(disputeID, 0, 2, crowdfunder2, 0.41 ether); - disputeKit.withdrawFeesAndRewards(disputeID, payable(crowdfunder2), 0, 2); + emit DisputeKitClassicBase.Withdrawal(disputeID, 2, crowdfunder2, 0.41 ether); + disputeKit.withdrawFeesAndRewards(disputeID, payable(crowdfunder2), 2); assertEq(crowdfunder1.balance, 10 ether, "Wrong balance of the crowdfunder1"); assertEq(crowdfunder2.balance, 10 ether, "Wrong balance of the crowdfunder2"); diff --git a/contracts/test/foundry/KlerosCore_Voting.t.sol b/contracts/test/foundry/KlerosCore_Voting.t.sol index 961d0741d..689df3a7d 100644 --- a/contracts/test/foundry/KlerosCore_Voting.t.sol +++ b/contracts/test/foundry/KlerosCore_Voting.t.sol @@ -456,13 +456,14 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { core.passPeriod(disputeID); // Vote // Check that the new DK has the info but not the old one. - - assertEq(disputeKit.coreDisputeIDToActive(disputeID), false, "Should be false for old DK"); + (bool disputeActive, ) = disputeKit.coreDisputeIDToActive(disputeID); + assertEq(disputeActive, false, "Should be false for old DK"); // This is the DK where dispute was created. Core dispute points to index 1 because new DK has two disputes. + (disputeActive, ) = newDisputeKit.coreDisputeIDToActive(disputeID); + assertEq(disputeActive, true, "Should be active for new DK"); assertEq(newDisputeKit.coreDisputeIDToLocal(disputeID), 1, "Wrong local dispute ID for new DK"); - assertEq(newDisputeKit.coreDisputeIDToActive(disputeID), true, "Should be active for new DK"); - (uint256 numberOfChoices, , bytes memory extraData) = newDisputeKit.disputes(1); + (uint256 numberOfChoices, bytes memory extraData) = newDisputeKit.disputes(1); assertEq(numberOfChoices, 2, "Wrong numberOfChoices in new DK"); assertEq(extraData, newExtraData, "Wrong extra data"); @@ -473,7 +474,7 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { // Deliberately cast votes using the old DK to see if the exception will be caught. vm.prank(staker1); - vm.expectRevert(DisputeKitClassicBase.NotActiveForCoreDisputeID.selector); + vm.expectRevert(DisputeKitClassicBase.DisputeUnknownInThisDisputeKit.selector); disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); // And check the new DK.