Skip to content

Commit 1aae950

Browse files
committed
feat: unstake juror if below minStake after penalty, simplified sub-court origination logic
1 parent 5468d55 commit 1aae950

File tree

5 files changed

+74
-111
lines changed

5 files changed

+74
-111
lines changed

contracts/src/arbitration/KlerosCoreBase.sol

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable
789789

790790
// Apply the penalty to the staked PNKs.
791791
uint96 penalizedInCourtID = round.drawnJurorFromCourtIDs[_params.repartition];
792-
(uint256 pnkBalance, uint256 availablePenalty) = sortitionModule.setStakePenalty(
792+
(uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) = sortitionModule.setStakePenalty(
793793
account,
794794
penalizedInCourtID,
795795
penalty
@@ -804,10 +804,15 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable
804804
0,
805805
round.feeToken
806806
);
807-
// Unstake the juror from all courts if he was inactive or his balance can't cover penalties anymore.
807+
808808
if (pnkBalance == 0 || !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) {
809+
// The juror is inactive or their balance is can't cover penalties anymore, unstake them from all courts.
809810
sortitionModule.setJurorInactive(account);
811+
} else if (newCourtStake < courts[penalizedInCourtID].minStake) {
812+
// The juror's balance fell below the court minStake, unstake them from the court.
813+
sortitionModule.setStake(account, penalizedInCourtID, 0, 0, 0);
810814
}
815+
811816
if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) {
812817
// No one was coherent, send the rewards to the governor.
813818
_transferFeeToken(round.feeToken, payable(governor), round.totalFeesForJurors);

contracts/src/arbitration/SortitionModuleBase.sol

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

20-
struct SubCourtStakes {
21-
uint256 totalStakedInSubCourts;
22-
uint96[MAX_STAKE_PATHS] subCourtIDs;
23-
uint256[MAX_STAKE_PATHS] stakedInSubCourts;
24-
}
25-
2620
struct SortitionSumTree {
2721
uint256 K; // The maximum number of children per node.
2822
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.
2923
uint256[] nodes; // The tree nodes.
3024
// 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.
3125
mapping(bytes32 stakePathID => uint256 nodeIndex) IDsToNodeIndexes;
3226
mapping(uint256 nodeIndex => bytes32 stakePathID) nodeIndexesToIDs;
33-
mapping(bytes32 stakePathID => SubCourtStakes subcourtStakes) IDsToSubCourtStakes;
3427
}
3528

3629
struct DelayedStake {
@@ -322,27 +315,30 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
322315
/// @param _courtID The ID of the court.
323316
/// @param _penalty The amount of PNK to be deducted.
324317
/// @return pnkBalance The updated total PNK balance of the juror, including the penalty.
318+
/// @return newCourtStake The updated stake of the juror in the court.
325319
/// @return availablePenalty The amount of PNK that was actually deducted.
326320
function setStakePenalty(
327321
address _account,
328322
uint96 _courtID,
329323
uint256 _penalty
330-
) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) {
324+
) external override onlyByCore returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) {
331325
Juror storage juror = jurors[_account];
332326
availablePenalty = _penalty;
327+
newCourtStake = stakeOf(_account, _courtID);
333328
if (juror.stakedPnk < _penalty) {
334329
availablePenalty = juror.stakedPnk;
335330
}
336331

337-
if (availablePenalty == 0) return (juror.stakedPnk, 0); // No penalty to apply.
332+
if (availablePenalty == 0) return (juror.stakedPnk, newCourtStake, 0); // No penalty to apply.
338333

339334
uint256 currentStake = stakeOf(_account, _courtID);
340335
uint256 newStake = 0;
341336
if (currentStake >= availablePenalty) {
342337
newStake = currentStake - availablePenalty;
343338
}
344339
_setStake(_account, _courtID, 0, availablePenalty, newStake);
345-
pnkBalance = juror.stakedPnk; // Updated by _setStake().
340+
pnkBalance = juror.stakedPnk; // updated by _setStake()
341+
newCourtStake = stakeOf(_account, _courtID); // updated by _setStake()
346342
}
347343

348344
function _setStake(
@@ -378,14 +374,12 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
378374
bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID);
379375
bool finished = false;
380376
uint96 currentCourtID = _courtID;
381-
uint96 fromSubCourtID = 0; // 0 means it is not coming from a subcourt.
382377
while (!finished) {
383378
// Tokens are also implicitly staked in parent courts through sortition module to increase the chance of being drawn.
384-
_set(bytes32(uint256(currentCourtID)), _newStake, stakePathID, fromSubCourtID);
379+
_set(bytes32(uint256(currentCourtID)), _newStake, stakePathID);
385380
if (currentCourtID == GENERAL_COURT) {
386381
finished = true;
387382
} else {
388-
fromSubCourtID = currentCourtID;
389383
(currentCourtID, , , , , , ) = core.courts(currentCourtID); // Get the parent court.
390384
}
391385
}
@@ -486,31 +480,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
486480
}
487481

488482
bytes32 stakePathID = tree.nodeIndexesToIDs[treeIndex];
489-
drawnAddress = _stakePathIDToAccount(stakePathID);
490-
491-
// Identify which subcourt was selected based on currentDrawnNumber
492-
SubCourtStakes storage subcourtStakes = tree.IDsToSubCourtStakes[stakePathID];
493-
494-
// The current court stake is the node value minus all subcourt stakes
495-
uint256 currentCourtStake = 0;
496-
if (tree.nodes[treeIndex] > subcourtStakes.totalStakedInSubCourts) {
497-
currentCourtStake = tree.nodes[treeIndex] - subcourtStakes.totalStakedInSubCourts;
498-
}
499-
500-
// Check if the drawn number falls within current court range
501-
if (currentDrawnNumber >= currentCourtStake) {
502-
// Find which subcourt range contains the drawn number
503-
uint256 accumulatedStake = currentCourtStake;
504-
for (uint256 i = 0; i < MAX_STAKE_PATHS; i++) {
505-
if (subcourtStakes.stakedInSubCourts[i] > 0) {
506-
if (currentDrawnNumber < accumulatedStake + subcourtStakes.stakedInSubCourts[i]) {
507-
fromSubcourtID = subcourtStakes.subCourtIDs[i];
508-
break;
509-
}
510-
accumulatedStake += subcourtStakes.stakedInSubCourts[i];
511-
}
512-
}
513-
}
483+
(drawnAddress, fromSubcourtID) = _stakePathIDToAccountAndCourtID(stakePathID);
514484
}
515485

516486
/// @dev Get the stake of a juror in a court.
@@ -524,11 +494,11 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
524494

525495
/// @dev Get the stake of a juror in a court.
526496
/// @param _key The key of the tree, corresponding to a court.
527-
/// @param _ID The stake path ID, corresponding to a juror.
497+
/// @param _stakePathID The stake path ID, corresponding to a juror.
528498
/// @return The stake of the juror in the court.
529-
function stakeOf(bytes32 _key, bytes32 _ID) public view returns (uint256) {
499+
function stakeOf(bytes32 _key, bytes32 _stakePathID) public view returns (uint256) {
530500
SortitionSumTree storage tree = sortitionSumTrees[_key];
531-
uint treeIndex = tree.IDsToNodeIndexes[_ID];
501+
uint treeIndex = tree.IDsToNodeIndexes[_stakePathID];
532502
if (treeIndex == 0) {
533503
return 0;
534504
}
@@ -601,59 +571,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
601571
}
602572
}
603573

604-
/// @dev Update the subcourt stakes.
605-
/// @param _subcourtStakes The subcourt stakes.
606-
/// @param _value The value to update.
607-
/// @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.
608-
/// `O(1)` complexity with `MAX_STAKE_PATHS` a small constant.
609-
function _updateSubCourtStakes(
610-
SubCourtStakes storage _subcourtStakes,
611-
uint256 _value,
612-
uint96 _fromSubCourtID
613-
) internal {
614-
// Update existing stake item if found
615-
for (uint256 i = 0; i < MAX_STAKE_PATHS; i++) {
616-
if (_subcourtStakes.subCourtIDs[i] == _fromSubCourtID) {
617-
if (_value == 0) {
618-
delete _subcourtStakes.subCourtIDs[i];
619-
delete _subcourtStakes.stakedInSubCourts[i];
620-
} else {
621-
_subcourtStakes.totalStakedInSubCourts += _value;
622-
_subcourtStakes.totalStakedInSubCourts -= _subcourtStakes.stakedInSubCourts[i];
623-
_subcourtStakes.stakedInSubCourts[i] = _value;
624-
}
625-
return;
626-
}
627-
}
628-
// Not found so add a new stake item
629-
for (uint256 i = 0; i < MAX_STAKE_PATHS; i++) {
630-
if (_subcourtStakes.subCourtIDs[i] == 0) {
631-
_subcourtStakes.subCourtIDs[i] = _fromSubCourtID;
632-
_subcourtStakes.totalStakedInSubCourts += _value;
633-
_subcourtStakes.stakedInSubCourts[i] = _value;
634-
return;
635-
}
636-
}
637-
}
638-
639-
/// @dev Retrieves a juror's address from the stake path ID.
640-
/// @param _stakePathID The stake path ID to unpack.
641-
/// @return account The account.
642-
function _stakePathIDToAccount(bytes32 _stakePathID) internal pure returns (address account) {
643-
assembly {
644-
// solium-disable-line security/no-inline-assembly
645-
let ptr := mload(0x40)
646-
for {
647-
let i := 0x00
648-
} lt(i, 0x14) {
649-
i := add(i, 0x01)
650-
} {
651-
mstore8(add(add(ptr, 0x0c), i), byte(i, _stakePathID))
652-
}
653-
account := mload(ptr)
654-
}
655-
}
656-
657574
function _extraDataToTreeK(bytes memory _extraData) internal pure returns (uint256 K) {
658575
if (_extraData.length >= 32) {
659576
assembly {
@@ -668,14 +585,13 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
668585
/// @dev Set a value in a tree.
669586
/// @param _key The key of the tree.
670587
/// @param _value The new value.
671-
/// @param _ID The ID of the value.
672-
/// @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.
588+
/// @param _stakePathID The ID of the value.
673589
/// `O(log_k(n))` where
674590
/// `k` is the maximum number of children per node in the tree,
675591
/// and `n` is the maximum number of nodes ever appended.
676-
function _set(bytes32 _key, uint256 _value, bytes32 _ID, uint96 _fromSubCourtID) internal {
592+
function _set(bytes32 _key, uint256 _value, bytes32 _stakePathID) internal {
677593
SortitionSumTree storage tree = sortitionSumTrees[_key];
678-
uint256 treeIndex = tree.IDsToNodeIndexes[_ID];
594+
uint256 treeIndex = tree.IDsToNodeIndexes[_stakePathID];
679595

680596
if (treeIndex == 0) {
681597
// No existing node.
@@ -709,8 +625,8 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
709625
}
710626

711627
// Add label.
712-
tree.IDsToNodeIndexes[_ID] = treeIndex;
713-
tree.nodeIndexesToIDs[treeIndex] = _ID;
628+
tree.IDsToNodeIndexes[_stakePathID] = treeIndex;
629+
tree.nodeIndexesToIDs[treeIndex] = _stakePathID;
714630

715631
_updateParents(_key, treeIndex, true, _value);
716632
}
@@ -727,7 +643,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
727643
tree.stack.push(treeIndex);
728644

729645
// Clear label.
730-
delete tree.IDsToNodeIndexes[_ID];
646+
delete tree.IDsToNodeIndexes[_stakePathID];
731647
delete tree.nodeIndexesToIDs[treeIndex];
732648

733649
_updateParents(_key, treeIndex, false, value);
@@ -743,11 +659,9 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
743659
_updateParents(_key, treeIndex, plusOrMinus, plusOrMinusValue);
744660
}
745661
}
746-
747-
_updateSubCourtStakes(tree.IDsToSubCourtStakes[_ID], _value, _fromSubCourtID);
748662
}
749663

750-
/// @dev Packs an account and a court ID into a stake path ID.
664+
/// @dev Packs an account and a court ID into a stake path ID: [20 bytes of address][12 bytes of courtID] = 32 bytes total.
751665
/// @param _account The address of the juror to pack.
752666
/// @param _courtID The court ID to pack.
753667
/// @return stakePathID The stake path ID.
@@ -758,13 +672,17 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
758672
assembly {
759673
// solium-disable-line security/no-inline-assembly
760674
let ptr := mload(0x40)
675+
676+
// Write account address (first 20 bytes)
761677
for {
762678
let i := 0x00
763679
} lt(i, 0x14) {
764680
i := add(i, 0x01)
765681
} {
766682
mstore8(add(ptr, i), byte(add(0x0c, i), _account))
767683
}
684+
685+
// Write court ID (last 12 bytes)
768686
for {
769687
let i := 0x14
770688
} lt(i, 0x20) {
@@ -776,6 +694,39 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr
776694
}
777695
}
778696

697+
/// @dev Retrieves both juror's address and court ID from the stake path ID.
698+
/// @param _stakePathID The stake path ID to unpack.
699+
/// @return account The account.
700+
/// @return courtID The court ID.
701+
function _stakePathIDToAccountAndCourtID(
702+
bytes32 _stakePathID
703+
) internal pure returns (address account, uint96 courtID) {
704+
assembly {
705+
// solium-disable-line security/no-inline-assembly
706+
let ptr := mload(0x40)
707+
708+
// Read account address (first 20 bytes)
709+
for {
710+
let i := 0x00
711+
} lt(i, 0x14) {
712+
i := add(i, 0x01)
713+
} {
714+
mstore8(add(add(ptr, 0x0c), i), byte(i, _stakePathID))
715+
}
716+
account := mload(ptr)
717+
718+
// Read court ID (last 12 bytes)
719+
for {
720+
let i := 0x00
721+
} lt(i, 0x0c) {
722+
i := add(i, 0x01)
723+
} {
724+
mstore8(add(add(ptr, 0x14), i), byte(add(i, 0x14), _stakePathID))
725+
}
726+
courtID := mload(ptr)
727+
}
728+
}
729+
779730
// ************************************* //
780731
// * Errors * //
781732
// ************************************* //

contracts/src/arbitration/interfaces/ISortitionModule.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ interface ISortitionModule {
3333
address _account,
3434
uint96 _courtID,
3535
uint256 _penalty
36-
) external returns (uint256 pnkBalance, uint256 availablePenalty);
36+
) external returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty);
3737

3838
function setJurorInactive(address _account) external;
3939

contracts/src/arbitration/university/KlerosCoreUniversity.sol

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable {
777777

778778
// Apply the penalty to the staked PNKs.
779779
uint96 penalizedInCourtID = round.drawnJurorFromCourtIDs[_params.repartition];
780-
(uint256 pnkBalance, uint256 availablePenalty) = sortitionModule.setStakePenalty(
780+
(uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) = sortitionModule.setStakePenalty(
781781
account,
782782
penalizedInCourtID,
783783
penalty
@@ -792,10 +792,15 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable {
792792
0,
793793
round.feeToken
794794
);
795-
// Unstake the juror from all courts if he was inactive or his balance can't cover penalties anymore.
795+
796796
if (pnkBalance == 0 || !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) {
797+
// The juror is inactive or their balance is can't cover penalties anymore, unstake them from all courts.
797798
sortitionModule.setJurorInactive(account);
799+
} else if (newCourtStake < courts[penalizedInCourtID].minStake) {
800+
// The juror's balance fell below the court minStake, unstake them from the court.
801+
sortitionModule.setStake(account, penalizedInCourtID, 0, 0, 0);
798802
}
803+
799804
if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) {
800805
// No one was coherent, send the rewards to the governor.
801806
if (round.feeToken == NATIVE_CURRENCY) {

contracts/src/arbitration/university/SortitionModuleUniversity.sol

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,15 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable,
197197
address _account,
198198
uint96 _courtID,
199199
uint256 _penalty
200-
) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) {
200+
) external override onlyByCore returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) {
201201
Juror storage juror = jurors[_account];
202202
availablePenalty = _penalty;
203+
newCourtStake = _stakeOf(_account, _courtID);
203204
if (juror.stakedPnk < _penalty) {
204205
availablePenalty = juror.stakedPnk;
205206
}
206207

207-
if (availablePenalty == 0) return (juror.stakedPnk, 0); // No penalty to apply.
208+
if (availablePenalty == 0) return (juror.stakedPnk, newCourtStake, 0); // No penalty to apply.
208209

209210
uint256 currentStake = _stakeOf(_account, _courtID);
210211
uint256 newStake = 0;
@@ -213,6 +214,7 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable,
213214
}
214215
_setStake(_account, _courtID, 0, availablePenalty, newStake);
215216
pnkBalance = juror.stakedPnk; // updated by _setStake()
217+
newCourtStake = _stakeOf(_account, _courtID); // updated by _setStake()
216218
}
217219

218220
function _setStake(

0 commit comments

Comments
 (0)