Skip to content

Commit a934ba5

Browse files
authored
Merge pull request #2159 from kleros/feat/recurring-dk-test
Complex DK jump sequences bug fix and extended testing
2 parents decd3e7 + a0a3a67 commit a934ba5

File tree

8 files changed

+399
-82
lines changed

8 files changed

+399
-82
lines changed

contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol

Lines changed: 58 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
2424
struct Dispute {
2525
Round[] rounds; // Rounds of the dispute. 0 is the default round, and [1, ..n] are the appeal rounds.
2626
uint256 numberOfChoices; // The number of choices jurors have when voting. This does not include choice `0` which is reserved for "refuse to arbitrate".
27-
bool jumped; // True if dispute jumped to a parent dispute kit and won't be handled by this DK anymore.
2827
mapping(uint256 => uint256) coreRoundIDToLocal; // Maps id of the round in the core contract to the index of the round of related local dispute.
2928
bytes extraData; // Extradata for the dispute.
3029
uint256[10] __gap; // Reserved slots for future upgrades.
@@ -54,6 +53,11 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
5453
uint256[10] __gap; // Reserved slots for future upgrades.
5554
}
5655

56+
struct Active {
57+
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.
58+
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.
59+
}
60+
5761
// ************************************* //
5862
// * Storage * //
5963
// ************************************* //
@@ -67,7 +71,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
6771
Dispute[] public disputes; // Array of the locally created disputes.
6872
mapping(uint256 => uint256) public coreDisputeIDToLocal; // Maps the dispute ID in Kleros Core to the local dispute ID.
6973
bool public singleDrawPerJuror; // Whether each juror can only draw once per dispute, false by default.
70-
mapping(uint256 coreDisputeID => bool) public coreDisputeIDToActive; // True if this dispute kit is active for this core dispute ID.
74+
mapping(uint256 coreDisputeID => Active) public coreDisputeIDToActive; // Active status of the dispute and the current round.
7175
address public wNative; // The wrapped native token for safeSend().
7276
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.
7377

@@ -106,17 +110,10 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
106110

107111
/// @notice To be emitted when the contributed funds are withdrawn.
108112
/// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract.
109-
/// @param _coreRoundID The identifier of the round in the Arbitrator contract.
110113
/// @param _choice The choice that is being funded.
111114
/// @param _contributor The address of the contributor.
112115
/// @param _amount The amount withdrawn.
113-
event Withdrawal(
114-
uint256 indexed _coreDisputeID,
115-
uint256 indexed _coreRoundID,
116-
uint256 _choice,
117-
address indexed _contributor,
118-
uint256 _amount
119-
);
116+
event Withdrawal(uint256 indexed _coreDisputeID, uint256 _choice, address indexed _contributor, uint256 _amount);
120117

121118
/// @notice To be emitted when a choice is fully funded for an appeal.
122119
/// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract.
@@ -138,8 +135,9 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
138135
_;
139136
}
140137

141-
modifier notJumped(uint256 _coreDisputeID) {
142-
if (disputes[coreDisputeIDToLocal[_coreDisputeID]].jumped) revert DisputeJumpedToParentDK();
138+
modifier isActive(uint256 _coreDisputeID) {
139+
if (!coreDisputeIDToActive[_coreDisputeID].dispute) revert DisputeUnknownInThisDisputeKit();
140+
if (!coreDisputeIDToActive[_coreDisputeID].currentRound) revert DisputeJumpedToAnotherDisputeKit();
143141
_;
144142
}
145143

@@ -206,28 +204,37 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
206204
bytes calldata _extraData,
207205
uint256 /*_nbVotes*/
208206
) external override onlyByCore {
209-
uint256 localDisputeID = disputes.length;
210-
Dispute storage dispute = disputes.push();
207+
uint256 localDisputeID;
208+
Dispute storage dispute;
209+
Active storage active = coreDisputeIDToActive[_coreDisputeID];
210+
if (active.dispute) {
211+
// 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.
212+
localDisputeID = coreDisputeIDToLocal[_coreDisputeID];
213+
dispute = disputes[localDisputeID];
214+
} else {
215+
// The dispute has not been created in this DK yet.
216+
localDisputeID = disputes.length;
217+
dispute = disputes.push();
218+
coreDisputeIDToLocal[_coreDisputeID] = localDisputeID;
219+
}
220+
221+
active.dispute = true;
222+
active.currentRound = true;
211223
dispute.numberOfChoices = _numberOfChoices;
212224
dispute.extraData = _extraData;
213-
dispute.jumped = false; // Possibly true if this DK has jumped in a previous round.
214225

215-
// New round in the Core should be created before the dispute creation in DK.
226+
// KlerosCore.Round must have been already created.
216227
dispute.coreRoundIDToLocal[core.getNumberOfRounds(_coreDisputeID) - 1] = dispute.rounds.length;
228+
dispute.rounds.push().tied = true;
217229

218-
Round storage round = dispute.rounds.push();
219-
round.tied = true;
220-
221-
coreDisputeIDToLocal[_coreDisputeID] = localDisputeID;
222-
coreDisputeIDToActive[_coreDisputeID] = true;
223230
emit DisputeCreation(_coreDisputeID, _numberOfChoices, _extraData);
224231
}
225232

226233
/// @inheritdoc IDisputeKit
227234
function draw(
228235
uint256 _coreDisputeID,
229236
uint256 _nonce
230-
) external override onlyByCore notJumped(_coreDisputeID) returns (address drawnAddress, uint96 fromSubcourtID) {
237+
) external override onlyByCore isActive(_coreDisputeID) returns (address drawnAddress, uint96 fromSubcourtID) {
231238
uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID];
232239
Dispute storage dispute = disputes[localDisputeID];
233240
uint256 localRoundID = dispute.rounds.length - 1;
@@ -266,11 +273,10 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
266273
uint256 _coreDisputeID,
267274
uint256[] calldata _voteIDs,
268275
bytes32 _commit
269-
) internal notJumped(_coreDisputeID) {
276+
) internal isActive(_coreDisputeID) {
270277
(, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID);
271278
if (period != KlerosCore.Period.commit) revert NotCommitPeriod();
272279
if (_commit == bytes32(0)) revert EmptyCommit();
273-
if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID();
274280

275281
Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]];
276282
Round storage round = dispute.rounds[dispute.rounds.length - 1];
@@ -313,11 +319,10 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
313319
uint256 _salt,
314320
string memory _justification,
315321
address _juror
316-
) internal notJumped(_coreDisputeID) {
322+
) internal isActive(_coreDisputeID) {
317323
(, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID);
318324
if (period != KlerosCore.Period.vote) revert NotVotePeriod();
319325
if (_voteIDs.length == 0) revert EmptyVoteIDs();
320-
if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID();
321326

322327
uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID];
323328
Dispute storage dispute = disputes[localDisputeID];
@@ -364,10 +369,9 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
364369
/// Note that the surplus deposit will be reimbursed.
365370
/// @param _coreDisputeID Index of the dispute in Kleros Core.
366371
/// @param _choice A choice that receives funding.
367-
function fundAppeal(uint256 _coreDisputeID, uint256 _choice) external payable notJumped(_coreDisputeID) {
372+
function fundAppeal(uint256 _coreDisputeID, uint256 _choice) external payable isActive(_coreDisputeID) {
368373
Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]];
369374
if (_choice > dispute.numberOfChoices) revert ChoiceOutOfBounds();
370-
if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID();
371375

372376
(uint256 appealPeriodStart, uint256 appealPeriodEnd) = core.appealPeriod(_coreDisputeID);
373377
if (block.timestamp < appealPeriodStart || block.timestamp >= appealPeriodEnd) revert NotAppealPeriod();
@@ -417,7 +421,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
417421

418422
if (core.isDisputeKitJumping(_coreDisputeID)) {
419423
// Don't create a new round in case of a jump, and remove local dispute from the flow.
420-
dispute.jumped = true;
424+
coreDisputeIDToActive[_coreDisputeID].currentRound = false;
421425
} else {
422426
// Don't subtract 1 from length since both round arrays haven't been updated yet.
423427
dispute.coreRoundIDToLocal[coreRoundID + 1] = dispute.rounds.length;
@@ -433,48 +437,50 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
433437

434438
/// @notice Allows those contributors who attempted to fund an appeal round to withdraw any reimbursable fees or rewards after the dispute gets resolved.
435439
/// @dev Withdrawals are not possible if the core contract is paused.
440+
/// @dev It can be called after the dispute has jumped to another dispute kit.
436441
/// @param _coreDisputeID Index of the dispute in Kleros Core contract.
437442
/// @param _beneficiary The address whose rewards to withdraw.
438-
/// @param _coreRoundID The round in the Kleros Core contract the caller wants to withdraw from.
439443
/// @param _choice The ruling option that the caller wants to withdraw from.
440444
/// @return amount The withdrawn amount.
441445
function withdrawFeesAndRewards(
442446
uint256 _coreDisputeID,
443447
address payable _beneficiary,
444-
uint256 _coreRoundID,
445448
uint256 _choice
446449
) external returns (uint256 amount) {
447450
(, , , bool isRuled, ) = core.disputes(_coreDisputeID);
448451
if (!isRuled) revert DisputeNotResolved();
449452
if (core.paused()) revert CoreIsPaused();
450-
if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID();
453+
if (!coreDisputeIDToActive[_coreDisputeID].dispute) revert DisputeUnknownInThisDisputeKit();
451454

452455
Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]];
453-
Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]];
454456
(uint256 finalRuling, , ) = core.currentRuling(_coreDisputeID);
455457

456-
if (!round.hasPaid[_choice]) {
457-
// Allow to reimburse if funding was unsuccessful for this ruling option.
458-
amount = round.contributions[_beneficiary][_choice];
459-
} else {
460-
// Funding was successful for this ruling option.
461-
if (_choice == finalRuling) {
462-
// This ruling option is the ultimate winner.
463-
amount = round.paidFees[_choice] > 0
464-
? (round.contributions[_beneficiary][_choice] * round.feeRewards) / round.paidFees[_choice]
465-
: 0;
466-
} else if (!round.hasPaid[finalRuling]) {
467-
// The ultimate winner was not funded in this round. In this case funded ruling option(s) are reimbursed.
468-
amount =
469-
(round.contributions[_beneficiary][_choice] * round.feeRewards) /
470-
(round.paidFees[round.fundedChoices[0]] + round.paidFees[round.fundedChoices[1]]);
458+
for (uint256 i = 0; i < dispute.rounds.length; i++) {
459+
Round storage round = dispute.rounds[i];
460+
461+
if (!round.hasPaid[_choice]) {
462+
// Allow to reimburse if funding was unsuccessful for this ruling option.
463+
amount += round.contributions[_beneficiary][_choice];
464+
} else {
465+
// Funding was successful for this ruling option.
466+
if (_choice == finalRuling) {
467+
// This ruling option is the ultimate winner.
468+
amount += round.paidFees[_choice] > 0
469+
? (round.contributions[_beneficiary][_choice] * round.feeRewards) / round.paidFees[_choice]
470+
: 0;
471+
} else if (!round.hasPaid[finalRuling]) {
472+
// The ultimate winner was not funded in this round. In this case funded ruling option(s) are reimbursed.
473+
amount +=
474+
(round.contributions[_beneficiary][_choice] * round.feeRewards) /
475+
(round.paidFees[round.fundedChoices[0]] + round.paidFees[round.fundedChoices[1]]);
476+
}
471477
}
478+
round.contributions[_beneficiary][_choice] = 0;
472479
}
473-
round.contributions[_beneficiary][_choice] = 0;
474480

475481
if (amount != 0) {
476482
_beneficiary.safeSend(amount, wNative);
477-
emit Withdrawal(_coreDisputeID, _coreRoundID, _choice, _beneficiary, amount);
483+
emit Withdrawal(_coreDisputeID, _choice, _beneficiary, amount);
478484
}
479485
}
480486

@@ -748,11 +754,11 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
748754

749755
error OwnerOnly();
750756
error KlerosCoreOnly();
751-
error DisputeJumpedToParentDK();
757+
error DisputeJumpedToAnotherDisputeKit();
758+
error DisputeUnknownInThisDisputeKit();
752759
error UnsuccessfulCall();
753760
error NotCommitPeriod();
754761
error EmptyCommit();
755-
error NotActiveForCoreDisputeID();
756762
error JurorHasToOwnTheVote();
757763
error NotVotePeriod();
758764
error EmptyVoteIDs();

contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase {
118118
bytes32 _recoveryCommit,
119119
bytes32 _identity,
120120
bytes calldata _encryptedVote
121-
) external notJumped(_coreDisputeID) {
121+
) external {
122122
if (_recoveryCommit == bytes32(0)) revert EmptyRecoveryCommit();
123123

124124
uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID];
@@ -128,7 +128,7 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase {
128128
recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit;
129129
}
130130

131-
// `_castCommit()` ensures that the caller owns the vote
131+
// `_castCommit()` ensures that the caller owns the vote and that dispute is active
132132
_castCommit(_coreDisputeID, _voteIDs, _commit);
133133
emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _recoveryCommit, _identity, _encryptedVote);
134134
}

contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ contract DisputeKitShutter is DisputeKitClassicBase {
102102
bytes32 _recoveryCommit,
103103
bytes32 _identity,
104104
bytes calldata _encryptedVote
105-
) external notJumped(_coreDisputeID) {
105+
) external {
106106
if (_recoveryCommit == bytes32(0)) revert EmptyRecoveryCommit();
107107

108108
uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID];
@@ -112,7 +112,7 @@ contract DisputeKitShutter is DisputeKitClassicBase {
112112
recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit;
113113
}
114114

115-
// `_castCommit()` ensures that the caller owns the vote
115+
// `_castCommit()` ensures that the caller owns the vote and that dispute is active
116116
_castCommit(_coreDisputeID, _voteIDs, _commit);
117117
emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _recoveryCommit, _identity, _encryptedVote);
118118
}

contracts/src/arbitration/interfaces/IDisputeKit.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ interface IDisputeKit {
3232

3333
/// @notice Creates a local dispute and maps it to the dispute ID in the Core contract.
3434
/// @dev Access restricted to Kleros Core only.
35+
/// @dev The new `KlerosCore.Round` must be created before calling this function.
3536
/// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit.
3637
/// @param _numberOfChoices Number of choices of the dispute
3738
/// @param _extraData Additional info about the dispute, for possible use in future dispute kits.

0 commit comments

Comments
 (0)