@@ -64,12 +64,14 @@ contract KlerosCore is IArbitratorV2 {
6464 uint256 sumFeeRewardPaid; // Total sum of arbitration fees paid to coherent jurors as a reward in this round.
6565 uint256 sumPnkRewardPaid; // Total sum of PNK paid to coherent jurors as a reward in this round.
6666 IERC20 feeToken; // The token used for paying fees in this round.
67+ uint256 drawIterations; // The number of iterations passed drawing the jurors for this round.
6768 }
6869
6970 struct Juror {
7071 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`.
71- mapping (uint96 => uint256 ) stakedPnk; // The amount of PNKs the juror has staked in the court in the form `stakedPnk[courtID]`.
72- mapping (uint96 => uint256 ) lockedPnk; // The amount of PNKs the juror has locked in the court in the form `lockedPnk[courtID]`.
72+ uint256 stakedPnk; // The juror's total amount of tokens staked in subcourts. Reflects actual pnk balance.
73+ uint256 lockedPnk; // The juror's total amount of tokens locked in disputes. Can reflect actual pnk balance when stakedPnk are fully withdrawn.
74+ mapping (uint96 => uint256 ) stakedPnkByCourt; // The amount of PNKs the juror has staked in the court in the form `stakedPnkByCourt[courtID]`.
7375 }
7476
7577 struct DisputeKitNode {
@@ -126,7 +128,7 @@ contract KlerosCore is IArbitratorV2 {
126128 // ************************************* //
127129
128130 event StakeSet (address indexed _address , uint256 _courtID , uint256 _amount );
129- event StakeDelayed (address indexed _address , uint256 _courtID , uint256 _amount , uint256 _penalty );
131+ event StakeDelayed (address indexed _address , uint256 _courtID , uint256 _amount );
130132 event NewPeriod (uint256 indexed _disputeID , Period _period );
131133 event AppealPossible (uint256 indexed _disputeID , IArbitrableV2 indexed _arbitrable );
132134 event AppealDecision (uint256 indexed _disputeID , IArbitrableV2 indexed _arbitrable );
@@ -481,14 +483,14 @@ contract KlerosCore is IArbitratorV2 {
481483
482484 /// @dev Sets the caller's stake in a court.
483485 /// @param _courtID The ID of the court.
484- /// @param _stake The new stake.
485- function setStake (uint96 _courtID , uint256 _stake ) external {
486- if (! _setStakeForAccount (msg .sender , _courtID, _stake, 0 )) revert StakingFailed ();
486+ /// @param _newStake The new stake.
487+ function setStake (uint96 _courtID , uint256 _newStake ) external {
488+ if (! _setStakeForAccount (msg .sender , _courtID, _newStake )) revert StakingFailed ();
487489 }
488490
489- function setStakeBySortitionModule (address _account , uint96 _courtID , uint256 _stake , uint256 _penalty ) external {
491+ function setStakeBySortitionModule (address _account , uint96 _courtID , uint256 _newStake ) external {
490492 if (msg .sender != address (sortitionModule)) revert WrongCaller ();
491- _setStakeForAccount (_account, _courtID, _stake, _penalty );
493+ _setStakeForAccount (_account, _courtID, _newStake );
492494 }
493495
494496 /// @inheritdoc IArbitratorV2
@@ -608,21 +610,21 @@ contract KlerosCore is IArbitratorV2 {
608610
609611 IDisputeKit disputeKit = disputeKitNodes[round.disputeKitID].disputeKit;
610612
611- uint256 startIndex = round.drawnJurors.length ;
612- uint256 endIndex = startIndex + _iterations <= round.nbVotes ? startIndex + _iterations : round.nbVotes;
613-
614- for (uint256 i = startIndex; i < endIndex; i++ ) {
615- address drawnAddress = disputeKit.draw (_disputeID);
616- if (drawnAddress != address (0 )) {
617- jurors[drawnAddress].lockedPnk[dispute.courtID] += round.pnkAtStakePerJuror;
618- emit Draw (drawnAddress, _disputeID, currentRound, round.drawnJurors.length );
619- round.drawnJurors.push (drawnAddress);
620-
621- if (round.drawnJurors.length == round.nbVotes) {
622- sortitionModule.postDrawHook (_disputeID, currentRound);
623- }
613+ uint256 startIndex = round.drawIterations; // for gas: less storage reads
614+ uint256 i;
615+ while (i < _iterations && round.drawnJurors.length < round.nbVotes) {
616+ address drawnAddress = disputeKit.draw (_disputeID, startIndex + i++ );
617+ if (drawnAddress == address (0 )) {
618+ continue ;
619+ }
620+ jurors[drawnAddress].lockedPnk += round.pnkAtStakePerJuror;
621+ emit Draw (drawnAddress, _disputeID, currentRound, round.drawnJurors.length );
622+ round.drawnJurors.push (drawnAddress);
623+ if (round.drawnJurors.length == round.nbVotes) {
624+ sortitionModule.postDrawHook (_disputeID, currentRound);
624625 }
625626 }
627+ round.drawIterations += i;
626628 }
627629
628630 /// @dev Appeals the ruling of a specified dispute.
@@ -763,16 +765,14 @@ contract KlerosCore is IArbitratorV2 {
763765
764766 // Unlock the PNKs affected by the penalty
765767 address account = round.drawnJurors[_params.repartition];
766- jurors[account].lockedPnk[dispute.courtID] -= penalty;
767-
768- // Apply the penalty to the staked PNKs
769- if (jurors[account].stakedPnk[dispute.courtID] >= courts[dispute.courtID].minStake + penalty) {
770- // The juror still has enough staked PNKs after penalty for this court.
771- uint256 newStake = jurors[account].stakedPnk[dispute.courtID] - penalty;
772- _setStakeForAccount (account, dispute.courtID, newStake, penalty);
773- } else if (jurors[account].stakedPnk[dispute.courtID] != 0 ) {
774- // The juror does not have enough staked PNKs after penalty for this court, unstake them.
775- _setStakeForAccount (account, dispute.courtID, 0 , penalty);
768+ jurors[account].lockedPnk -= penalty;
769+
770+ // Apply the penalty to the staked PNKs.
771+ // Note that lockedPnk will always cover penalty while stakedPnk can become lower after manual unstaking.
772+ if (jurors[account].stakedPnk >= penalty) {
773+ jurors[account].stakedPnk -= penalty;
774+ } else {
775+ jurors[account].stakedPnk = 0 ;
776776 }
777777 emit TokenAndETHShift (
778778 account,
@@ -832,10 +832,10 @@ contract KlerosCore is IArbitratorV2 {
832832 uint256 pnkLocked = (round.pnkAtStakePerJuror * degreeOfCoherence) / ALPHA_DIVISOR;
833833
834834 // Release the rest of the PNKs of the juror for this round.
835- jurors[account].lockedPnk[dispute.courtID] -= pnkLocked;
835+ jurors[account].lockedPnk -= pnkLocked;
836836
837837 // Give back the locked PNKs in case the juror fully unstaked earlier.
838- if (jurors[account].stakedPnk[dispute.courtID] == 0 ) {
838+ if (jurors[account].stakedPnk == 0 ) {
839839 pinakion.safeTransfer (account, pnkLocked);
840840 }
841841
@@ -973,38 +973,8 @@ contract KlerosCore is IArbitratorV2 {
973973 (ruling, tied, overridden) = disputeKit.currentRuling (_disputeID);
974974 }
975975
976- function getRoundInfo (
977- uint256 _disputeID ,
978- uint256 _round
979- )
980- external
981- view
982- returns (
983- uint256 disputeKitID ,
984- uint256 pnkAtStakePerJuror ,
985- uint256 totalFeesForJurors ,
986- uint256 nbVotes ,
987- uint256 repartitions ,
988- uint256 pnkPenalties ,
989- address [] memory drawnJurors ,
990- uint256 sumFeeRewardPaid ,
991- uint256 sumPnkRewardPaid ,
992- IERC20 feeToken
993- )
994- {
995- Round storage round = disputes[_disputeID].rounds[_round];
996- return (
997- round.disputeKitID,
998- round.pnkAtStakePerJuror,
999- round.totalFeesForJurors,
1000- round.nbVotes,
1001- round.repartitions,
1002- round.pnkPenalties,
1003- round.drawnJurors,
1004- round.sumFeeRewardPaid,
1005- round.sumPnkRewardPaid,
1006- round.feeToken
1007- );
976+ function getRoundInfo (uint256 _disputeID , uint256 _round ) external view returns (Round memory ) {
977+ return disputes[_disputeID].rounds[_round];
1008978 }
1009979
1010980 function getNumberOfRounds (uint256 _disputeID ) external view returns (uint256 ) {
@@ -1014,10 +984,11 @@ contract KlerosCore is IArbitratorV2 {
1014984 function getJurorBalance (
1015985 address _juror ,
1016986 uint96 _courtID
1017- ) external view returns (uint256 staked , uint256 locked , uint256 nbCourts ) {
987+ ) external view returns (uint256 totalStaked , uint256 totalLocked , uint256 stakedInCourt , uint256 nbCourts ) {
1018988 Juror storage juror = jurors[_juror];
1019- staked = juror.stakedPnk[_courtID];
1020- locked = juror.lockedPnk[_courtID];
989+ totalStaked = juror.stakedPnk;
990+ totalLocked = juror.lockedPnk;
991+ stakedInCourt = juror.stakedPnkByCourt[_courtID];
1021992 nbCourts = juror.courtIDs.length ;
1022993 }
1023994
@@ -1109,77 +1080,80 @@ contract KlerosCore is IArbitratorV2 {
11091080 /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously.
11101081 /// @param _account The address of the juror.
11111082 /// @param _courtID The ID of the court.
1112- /// @param _stake The new stake.
1113- /// @param _penalty Penalized amount won't be transferred back to juror when the stake is lowered.
1083+ /// @param _newStake The new stake.
11141084 /// @return succeeded True if the call succeeded, false otherwise.
11151085 function _setStakeForAccount (
11161086 address _account ,
11171087 uint96 _courtID ,
1118- uint256 _stake ,
1119- uint256 _penalty
1088+ uint256 _newStake
11201089 ) internal returns (bool succeeded ) {
11211090 if (_courtID == FORKING_COURT || _courtID > courts.length ) return false ;
11221091
11231092 Juror storage juror = jurors[_account];
1124- uint256 currentStake = juror.stakedPnk [_courtID];
1093+ uint256 currentStake = juror.stakedPnkByCourt [_courtID];
11251094
1126- if (_stake != 0 ) {
1127- // Check against locked PNKs in case the min stake was lowered.
1128- if (_stake < courts[_courtID].minStake || _stake < juror.lockedPnk[_courtID]) return false ;
1095+ if (_newStake != 0 ) {
1096+ if (_newStake < courts[_courtID].minStake) return false ;
1097+ } else if (currentStake == 0 ) {
1098+ return false ;
11291099 }
11301100
1131- ISortitionModule.preStakeHookResult result = sortitionModule.preStakeHook (_account, _courtID, _stake, _penalty );
1101+ ISortitionModule.preStakeHookResult result = sortitionModule.preStakeHook (_account, _courtID, _newStake );
11321102 if (result == ISortitionModule.preStakeHookResult.failed) {
11331103 return false ;
11341104 } else if (result == ISortitionModule.preStakeHookResult.delayed) {
1135- emit StakeDelayed (_account, _courtID, _stake, _penalty );
1105+ emit StakeDelayed (_account, _courtID, _newStake );
11361106 return true ;
11371107 }
11381108
11391109 uint256 transferredAmount;
1140- if (_stake >= currentStake) {
1141- transferredAmount = _stake - currentStake;
1110+ if (_newStake >= currentStake) {
1111+ // Stake increase
1112+ // When stakedPnk becomes lower than lockedPnk count the locked tokens in when transferring tokens from juror.
1113+ // (E.g. stakedPnk = 0, lockedPnk = 150) which can happen if the juror unstaked fully while having some tokens locked.
1114+ uint256 previouslyLocked = (juror.lockedPnk >= juror.stakedPnk) ? juror.lockedPnk - juror.stakedPnk : 0 ; // underflow guard
1115+ transferredAmount = (_newStake >= currentStake + previouslyLocked) // underflow guard
1116+ ? _newStake - currentStake - previouslyLocked
1117+ : 0 ;
11421118 if (transferredAmount > 0 ) {
1143- if (pinakion.safeTransferFrom (_account, address (this ), transferredAmount)) {
1144- if (currentStake == 0 ) {
1145- juror.courtIDs.push (_courtID);
1146- }
1147- } else {
1119+ if (! pinakion.safeTransferFrom (_account, address (this ), transferredAmount)) {
11481120 return false ;
11491121 }
11501122 }
1123+ if (currentStake == 0 ) {
1124+ juror.courtIDs.push (_courtID);
1125+ }
11511126 } else {
1152- if (_stake == 0 ) {
1153- // Keep locked PNKs in the contract and release them after dispute is executed.
1154- transferredAmount = currentStake - juror.lockedPnk[_courtID] - _penalty;
1155- if (transferredAmount > 0 ) {
1156- if (pinakion.safeTransfer (_account, transferredAmount)) {
1157- for (uint256 i = juror.courtIDs.length ; i > 0 ; i-- ) {
1158- if (juror.courtIDs[i - 1 ] == _courtID) {
1159- juror.courtIDs[i - 1 ] = juror.courtIDs[juror.courtIDs.length - 1 ];
1160- juror.courtIDs.pop ();
1161- break ;
1162- }
1163- }
1164- } else {
1165- return false ;
1166- }
1127+ // Stake decrease: make sure locked tokens always stay in the contract. They can only be released during Execution.
1128+ if (juror.stakedPnk >= currentStake - _newStake + juror.lockedPnk) {
1129+ // We have enough pnk staked to afford withdrawal while keeping locked tokens.
1130+ transferredAmount = currentStake - _newStake;
1131+ } else if (juror.stakedPnk >= juror.lockedPnk) {
1132+ // Can't afford withdrawing the current stake fully. Take whatever is available while keeping locked tokens.
1133+ transferredAmount = juror.stakedPnk - juror.lockedPnk;
1134+ }
1135+ if (transferredAmount > 0 ) {
1136+ if (! pinakion.safeTransfer (_account, transferredAmount)) {
1137+ return false ;
11671138 }
1168- } else {
1169- transferredAmount = currentStake - _stake - _penalty;
1170- if (transferredAmount > 0 ) {
1171- if (! pinakion.safeTransfer (_account, transferredAmount)) {
1172- return false ;
1139+ }
1140+ if (_newStake == 0 ) {
1141+ for (uint256 i = juror.courtIDs.length ; i > 0 ; i-- ) {
1142+ if (juror.courtIDs[i - 1 ] == _courtID) {
1143+ juror.courtIDs[i - 1 ] = juror.courtIDs[juror.courtIDs.length - 1 ];
1144+ juror.courtIDs.pop ();
1145+ break ;
11731146 }
11741147 }
11751148 }
11761149 }
11771150
1178- // Update juror's records.
1179- juror.stakedPnk[_courtID] = _stake;
1151+ // Note that stakedPnk can become async with currentStake (e.g. after penalty).
1152+ juror.stakedPnk = (juror.stakedPnk >= currentStake) ? juror.stakedPnk - currentStake + _newStake : _newStake;
1153+ juror.stakedPnkByCourt[_courtID] = _newStake;
11801154
1181- sortitionModule.setStake (_account, _courtID, _stake );
1182- emit StakeSet (_account, _courtID, _stake );
1155+ sortitionModule.setStake (_account, _courtID, _newStake );
1156+ emit StakeSet (_account, _courtID, _newStake );
11831157 return true ;
11841158 }
11851159
0 commit comments