Skip to content

Commit f2b2183

Browse files
committed
fix: penalties now applied to sortition tree stakes
1 parent f02d718 commit f2b2183

File tree

10 files changed

+190
-67
lines changed

10 files changed

+190
-67
lines changed

contracts/hardhat.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const config: HardhatUserConfig = {
3131
viaIR: process.env.VIA_IR !== "false", // Defaults to true
3232
optimizer: {
3333
enabled: true,
34-
runs: 10000,
34+
runs: 2000,
3535
},
3636
outputSelection: {
3737
"*": {

contracts/src/arbitration/KlerosCoreBase.sol

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable
6060
uint256 repartitions; // A counter of reward repartitions made in this round.
6161
uint256 pnkPenalties; // The amount of PNKs collected from penalties in this round.
6262
address[] drawnJurors; // Addresses of the jurors that were drawn in this round.
63+
uint96[] drawnJurorFromCourtIDs; // The courtIDs where the juror was drawn from, possibly their stake in a subcourt.
6364
uint256 sumFeeRewardPaid; // Total sum of arbitration fees paid to coherent jurors as a reward in this round.
6465
uint256 sumPnkRewardPaid; // Total sum of PNK paid to coherent jurors as a reward in this round.
6566
IERC20 feeToken; // The token used for paying fees in this round.
@@ -610,13 +611,14 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable
610611
uint256 startIndex = round.drawIterations; // for gas: less storage reads
611612
uint256 i;
612613
while (i < _iterations && round.drawnJurors.length < round.nbVotes) {
613-
address drawnAddress = disputeKit.draw(_disputeID, startIndex + i++);
614+
(address drawnAddress, uint96 fromSubcourtID) = disputeKit.draw(_disputeID, startIndex + i++);
614615
if (drawnAddress == address(0)) {
615616
continue;
616617
}
617618
sortitionModule.lockStake(drawnAddress, round.pnkAtStakePerJuror);
618619
emit Draw(drawnAddress, _disputeID, currentRound, round.drawnJurors.length);
619620
round.drawnJurors.push(drawnAddress);
621+
round.drawnJurorFromCourtIDs.push(fromSubcourtID != 0 ? fromSubcourtID : dispute.courtID);
620622
if (round.drawnJurors.length == round.nbVotes) {
621623
sortitionModule.postDrawHook(_disputeID, currentRound);
622624
}
@@ -786,7 +788,12 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable
786788
sortitionModule.unlockStake(account, penalty);
787789

788790
// Apply the penalty to the staked PNKs.
789-
(uint256 pnkBalance, uint256 availablePenalty) = sortitionModule.penalizeStake(account, penalty);
791+
uint96 penalizedInCourtID = round.drawnJurorFromCourtIDs[_params.repartition];
792+
(uint256 pnkBalance, uint256 availablePenalty) = sortitionModule.setStakePenalty(
793+
account,
794+
penalizedInCourtID,
795+
penalty
796+
);
790797
_params.pnkPenaltiesInRound += availablePenalty;
791798
emit TokenAndETHShift(
792799
account,

contracts/src/arbitration/SortitionModuleBase.sol

Lines changed: 112 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,20 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
1717
// * Enums / Structs * //
1818
// ************************************* //
1919

20+
struct SubCourtStakes {
21+
uint256 totalStakedInCourts;
22+
uint96[MAX_STAKE_PATHS] courtIDs;
23+
uint256[MAX_STAKE_PATHS] stakedInCourts;
24+
}
25+
2026
struct SortitionSumTree {
2127
uint256 K; // The maximum number of children per node.
22-
// We use this to keep track of vacant positions in the tree after removing a leaf. This is for keeping the tree as balanced as possible without spending gas on moving nodes around.
23-
uint256[] stack;
24-
uint256[] nodes;
28+
uint256[] stack; // We use this to keep track of vacant positions in the tree after removing a leaf. This is for keeping the tree as balanced as possible without spending gas on moving nodes around.
29+
uint256[] nodes; // The tree nodes.
2530
// Two-way mapping of IDs to node indexes. Note that node index 0 is reserved for the root node, and means the ID does not have a node.
26-
mapping(bytes32 => uint256) IDsToNodeIndexes;
27-
mapping(uint256 => bytes32) nodeIndexesToIDs;
31+
mapping(bytes32 stakePathID => uint256 nodeIndex) IDsToNodeIndexes;
32+
mapping(uint256 nodeIndex => bytes32 stakePathID) nodeIndexesToIDs;
33+
mapping(bytes32 stakePathID => SubCourtStakes subcourtStakes) IDsToSubCourtStakes;
2834
}
2935

3036
struct DelayedStake {
@@ -36,7 +42,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
3642

3743
struct Juror {
3844
uint96[] courtIDs; // The IDs of courts where the juror's stake path ends. A stake path is a path from the general court to a court the juror directly staked in using `_setStake`.
39-
uint256 stakedPnk; // The juror's total amount of tokens staked in subcourts. Reflects actual pnk balance.
45+
uint256 stakedPnk; // The juror's total amount of tokens staked in subcourts. PNK balance including locked PNK and penalty deductions.
4046
uint256 lockedPnk; // The juror's total amount of tokens locked in disputes.
4147
}
4248

@@ -306,6 +312,28 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
306312
_setStake(_account, _courtID, _pnkDeposit, _pnkWithdrawal, _newStake);
307313
}
308314

315+
function setStakePenalty(
316+
address _account,
317+
uint96 _courtID,
318+
uint256 _penalty
319+
) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) {
320+
Juror storage juror = jurors[_account];
321+
availablePenalty = _penalty;
322+
if (juror.stakedPnk < _penalty) {
323+
availablePenalty = juror.stakedPnk;
324+
}
325+
326+
if (availablePenalty == 0) return (juror.stakedPnk, 0); // No penalty to apply.
327+
328+
uint256 currentStake = stakeOf(_account, _courtID);
329+
uint256 newStake = 0;
330+
if (currentStake >= availablePenalty) {
331+
newStake = currentStake - availablePenalty;
332+
}
333+
_setStake(_account, _courtID, 0, availablePenalty, newStake);
334+
pnkBalance = juror.stakedPnk; // updated by _setStake()
335+
}
336+
309337
function _setStake(
310338
address _account,
311339
uint96 _courtID,
@@ -339,12 +367,14 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
339367
bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID);
340368
bool finished = false;
341369
uint96 currentCourtID = _courtID;
370+
uint96 fromSubCourtID = 0; // 0 means it is not coming from a subcourt.
342371
while (!finished) {
343372
// Tokens are also implicitly staked in parent courts through sortition module to increase the chance of being drawn.
344-
_set(bytes32(uint256(currentCourtID)), _newStake, stakePathID);
373+
_set(bytes32(uint256(currentCourtID)), _newStake, stakePathID, fromSubCourtID);
345374
if (currentCourtID == GENERAL_COURT) {
346375
finished = true;
347376
} else {
377+
fromSubCourtID = currentCourtID;
348378
(currentCourtID, , , , , , ) = core.courts(currentCourtID); // Get the parent court.
349379
}
350380
}
@@ -367,25 +397,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
367397
}
368398
}
369399

370-
function penalizeStake(
371-
address _account,
372-
uint256 _relativeAmount
373-
) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) {
374-
Juror storage juror = jurors[_account];
375-
uint256 stakedPnk = juror.stakedPnk;
376-
377-
if (stakedPnk >= _relativeAmount) {
378-
availablePenalty = _relativeAmount;
379-
juror.stakedPnk -= _relativeAmount;
380-
} else {
381-
availablePenalty = stakedPnk;
382-
juror.stakedPnk = 0;
383-
}
384-
385-
pnkBalance = juror.stakedPnk;
386-
return (pnkBalance, availablePenalty);
387-
}
388-
389400
/// @dev Unstakes the inactive juror from all courts.
390401
/// `O(n * (p * log_k(j)) )` where
391402
/// `n` is the number of courts the juror has staked in,
@@ -433,12 +444,12 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
433444
bytes32 _key,
434445
uint256 _coreDisputeID,
435446
uint256 _nonce
436-
) public view override returns (address drawnAddress) {
447+
) public view override returns (address drawnAddress, uint96 fromSubcourtID) {
437448
if (phase != Phase.drawing) revert NotDrawingPhase();
438449
SortitionSumTree storage tree = sortitionSumTrees[_key];
439450

440451
if (tree.nodes[0] == 0) {
441-
return address(0); // No jurors staked.
452+
return (address(0), 0); // No jurors staked.
442453
}
443454

444455
uint256 currentDrawnNumber = uint256(keccak256(abi.encodePacked(randomNumber, _coreDisputeID, _nonce))) %
@@ -462,7 +473,33 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
462473
}
463474
}
464475
}
465-
drawnAddress = _stakePathIDToAccount(tree.nodeIndexesToIDs[treeIndex]);
476+
477+
bytes32 stakePathID = tree.nodeIndexesToIDs[treeIndex];
478+
drawnAddress = _stakePathIDToAccount(stakePathID);
479+
480+
// Identify which subcourt was selected based on currentDrawnNumber
481+
SubCourtStakes storage subcourtStakes = tree.IDsToSubCourtStakes[stakePathID];
482+
483+
// The current court stake is the node value minus all subcourt stakes
484+
uint256 currentCourtStake = 0;
485+
if (tree.nodes[treeIndex] > subcourtStakes.totalStakedInCourts) {
486+
currentCourtStake = tree.nodes[treeIndex] - subcourtStakes.totalStakedInCourts;
487+
}
488+
489+
// Check if the drawn number falls within current court range
490+
if (currentDrawnNumber >= currentCourtStake) {
491+
// Find which subcourt range contains the drawn number
492+
uint256 accumulatedStake = currentCourtStake;
493+
for (uint256 i = 0; i < MAX_STAKE_PATHS; i++) {
494+
if (subcourtStakes.stakedInCourts[i] > 0) {
495+
if (currentDrawnNumber < accumulatedStake + subcourtStakes.stakedInCourts[i]) {
496+
fromSubcourtID = subcourtStakes.courtIDs[i];
497+
break;
498+
}
499+
accumulatedStake += subcourtStakes.stakedInCourts[i];
500+
}
501+
}
502+
}
466503
}
467504

468505
/// @dev Get the stake of a juror in a court.
@@ -487,6 +524,13 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
487524
return tree.nodes[treeIndex];
488525
}
489526

527+
/// @dev Gets the balance of a juror in a court.
528+
/// @param _juror The address of the juror.
529+
/// @param _courtID The ID of the court.
530+
/// @return totalStaked The total amount of tokens staked including locked tokens and penalty deductions. Equivalent to the effective stake in the General court.
531+
/// @return totalLocked The total amount of tokens locked in disputes.
532+
/// @return stakedInCourt The amount of tokens staked in the specified court including locked tokens and penalty deductions.
533+
/// @return nbCourts The number of courts the juror has directly staked in.
490534
function getJurorBalance(
491535
address _juror,
492536
uint96 _courtID
@@ -546,6 +590,41 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
546590
}
547591
}
548592

593+
/// @dev Update the subcourt stakes.
594+
/// @param _subcourtStakes The subcourt stakes.
595+
/// @param _value The value to update.
596+
/// @param _fromSubCourtID The ID of the subcourt from which the stake is coming from, or 0 if the stake does not come from a subcourt.
597+
/// `O(1)` complexity with `MAX_STAKE_PATHS` a small constant.
598+
function _updateSubCourtStakes(
599+
SubCourtStakes storage _subcourtStakes,
600+
uint256 _value,
601+
uint96 _fromSubCourtID
602+
) internal {
603+
// Update existing stake item if found
604+
for (uint256 i = 0; i < MAX_STAKE_PATHS; i++) {
605+
if (_subcourtStakes.courtIDs[i] == _fromSubCourtID) {
606+
if (_value == 0) {
607+
delete _subcourtStakes.courtIDs[i];
608+
delete _subcourtStakes.stakedInCourts[i];
609+
} else {
610+
_subcourtStakes.totalStakedInCourts += _value;
611+
_subcourtStakes.totalStakedInCourts -= _subcourtStakes.stakedInCourts[i];
612+
_subcourtStakes.stakedInCourts[i] = _value;
613+
}
614+
return;
615+
}
616+
}
617+
// Not found so add a new stake item
618+
for (uint256 i = 0; i < MAX_STAKE_PATHS; i++) {
619+
if (_subcourtStakes.courtIDs[i] == 0) {
620+
_subcourtStakes.courtIDs[i] = _fromSubCourtID;
621+
_subcourtStakes.totalStakedInCourts += _value;
622+
_subcourtStakes.stakedInCourts[i] = _value;
623+
return;
624+
}
625+
}
626+
}
627+
549628
/// @dev Retrieves a juror's address from the stake path ID.
550629
/// @param _stakePathID The stake path ID to unpack.
551630
/// @return account The account.
@@ -579,10 +658,11 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
579658
/// @param _key The key of the tree.
580659
/// @param _value The new value.
581660
/// @param _ID The ID of the value.
661+
/// @param _fromSubCourtID The ID of the subcourt from which the stake is coming from, or 0 if the stake does not come from a subcourt.
582662
/// `O(log_k(n))` where
583663
/// `k` is the maximum number of children per node in the tree,
584664
/// and `n` is the maximum number of nodes ever appended.
585-
function _set(bytes32 _key, uint256 _value, bytes32 _ID) internal {
665+
function _set(bytes32 _key, uint256 _value, bytes32 _ID, uint96 _fromSubCourtID) internal {
586666
SortitionSumTree storage tree = sortitionSumTrees[_key];
587667
uint256 treeIndex = tree.IDsToNodeIndexes[_ID];
588668

@@ -652,6 +732,8 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
652732
_updateParents(_key, treeIndex, plusOrMinus, plusOrMinusValue);
653733
}
654734
}
735+
736+
_updateSubCourtStakes(tree.IDsToSubCourtStakes[_ID], _value, _fromSubCourtID);
655737
}
656738

657739
/// @dev Packs an account and a court ID into a stake path ID.

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
228228
function draw(
229229
uint256 _coreDisputeID,
230230
uint256 _nonce
231-
) external override onlyByCore notJumped(_coreDisputeID) returns (address drawnAddress) {
231+
) external override onlyByCore notJumped(_coreDisputeID) returns (address drawnAddress, uint96 fromSubcourtID) {
232232
uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID];
233233
Dispute storage dispute = disputes[localDisputeID];
234234
uint256 localRoundID = dispute.rounds.length - 1;
@@ -238,10 +238,10 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
238238
(uint96 courtID, , , , ) = core.disputes(_coreDisputeID);
239239
bytes32 key = bytes32(uint256(courtID)); // Get the ID of the tree.
240240

241-
drawnAddress = sortitionModule.draw(key, _coreDisputeID, _nonce);
241+
(drawnAddress, fromSubcourtID) = sortitionModule.draw(key, _coreDisputeID, _nonce);
242242
if (drawnAddress == address(0)) {
243243
// Sortition can return 0 address if no one has staked yet.
244-
return drawnAddress;
244+
return (drawnAddress, fromSubcourtID);
245245
}
246246

247247
if (_postDrawCheck(round, _coreDisputeID, drawnAddress)) {

contracts/src/arbitration/interfaces/IDisputeKit.sol

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ interface IDisputeKit {
4848
/// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit.
4949
/// @param _nonce Nonce.
5050
/// @return drawnAddress The drawn address.
51-
function draw(uint256 _coreDisputeID, uint256 _nonce) external returns (address drawnAddress);
51+
function draw(
52+
uint256 _coreDisputeID,
53+
uint256 _nonce
54+
) external returns (address drawnAddress, uint96 fromSubcourtID);
5255

5356
// ************************************* //
5457
// * Public Views * //

contracts/src/arbitration/interfaces/ISortitionModule.sol

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,25 @@ interface ISortitionModule {
2929
uint256 _newStake
3030
) external;
3131

32+
function setStakePenalty(
33+
address _account,
34+
uint96 _courtID,
35+
uint256 _penalty
36+
) external returns (uint256 pnkBalance, uint256 availablePenalty);
37+
3238
function setJurorInactive(address _account) external;
3339

3440
function lockStake(address _account, uint256 _relativeAmount) external;
3541

3642
function unlockStake(address _account, uint256 _relativeAmount) external;
3743

38-
function penalizeStake(
39-
address _account,
40-
uint256 _relativeAmount
41-
) external returns (uint256 pnkBalance, uint256 availablePenalty);
42-
4344
function notifyRandomNumber(uint256 _drawnNumber) external;
4445

45-
function draw(bytes32 _court, uint256 _coreDisputeID, uint256 _nonce) external view returns (address);
46+
function draw(
47+
bytes32 _court,
48+
uint256 _coreDisputeID,
49+
uint256 _nonce
50+
) external view returns (address drawnAddress, uint96 fromSubcourtID);
4651

4752
function getJurorBalance(
4853
address _juror,

contracts/src/arbitration/university/KlerosCoreUniversity.sol

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable {
5959
uint256 repartitions; // A counter of reward repartitions made in this round.
6060
uint256 pnkPenalties; // The amount of PNKs collected from penalties in this round.
6161
address[] drawnJurors; // Addresses of the jurors that were drawn in this round.
62+
uint96[] drawnJurorFromCourtIDs; // The courtIDs where the juror was drawn from, possibly their stake in a subcourt.
6263
uint256 sumFeeRewardPaid; // Total sum of arbitration fees paid to coherent jurors as a reward in this round.
6364
uint256 sumPnkRewardPaid; // Total sum of PNK paid to coherent jurors as a reward in this round.
6465
IERC20 feeToken; // The token used for paying fees in this round.
@@ -599,13 +600,14 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable {
599600
{
600601
IDisputeKit disputeKit = disputeKits[round.disputeKitID];
601602
uint256 iteration = round.drawIterations + 1;
602-
address drawnAddress = disputeKit.draw(_disputeID, iteration);
603+
(address drawnAddress, uint96 fromSubcourtID) = disputeKit.draw(_disputeID, iteration);
603604
if (drawnAddress == address(0)) {
604605
revert NoJurorDrawn();
605606
}
606607
sortitionModule.lockStake(drawnAddress, round.pnkAtStakePerJuror);
607608
emit Draw(drawnAddress, _disputeID, currentRound, round.drawnJurors.length);
608609
round.drawnJurors.push(drawnAddress);
610+
round.drawnJurorFromCourtIDs.push(fromSubcourtID != 0 ? fromSubcourtID : dispute.courtID);
609611
if (round.drawnJurors.length == round.nbVotes) {
610612
sortitionModule.postDrawHook(_disputeID, currentRound);
611613
}
@@ -774,7 +776,12 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable {
774776
sortitionModule.unlockStake(account, penalty);
775777

776778
// Apply the penalty to the staked PNKs.
777-
(uint256 pnkBalance, uint256 availablePenalty) = sortitionModule.penalizeStake(account, penalty);
779+
uint96 penalizedInCourtID = round.drawnJurorFromCourtIDs[_params.repartition];
780+
(uint256 pnkBalance, uint256 availablePenalty) = sortitionModule.setStakePenalty(
781+
account,
782+
penalizedInCourtID,
783+
penalty
784+
);
778785
_params.pnkPenaltiesInRound += availablePenalty;
779786
emit TokenAndETHShift(
780787
account,

0 commit comments

Comments
 (0)