@@ -68,8 +68,9 @@ contract KlerosCore is IArbitratorV2 {
6868
6969 struct Juror {
7070 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]`.
71+ uint256 stakedPnk; // The juror's total amount of tokens staked in subcourts. Reflects actual pnk balance.
72+ uint256 lockedPnk; // The juror's total amount of tokens locked in disputes. Can reflect actual pnk balance when stakedPnk are fully withdrawn.
73+ mapping (uint96 => uint256 ) stakedPnkByCourt; // The amount of PNKs the juror has staked in the court in the form `stakedPnkByCourt[courtID]`.
7374 }
7475
7576 struct DisputeKitNode {
@@ -126,7 +127,7 @@ contract KlerosCore is IArbitratorV2 {
126127 // ************************************* //
127128
128129 event StakeSet (address indexed _address , uint256 _courtID , uint256 _amount );
129- event StakeDelayed (address indexed _address , uint256 _courtID , uint256 _amount , uint256 _penalty );
130+ event StakeDelayed (address indexed _address , uint256 _courtID , uint256 _amount );
130131 event NewPeriod (uint256 indexed _disputeID , Period _period );
131132 event AppealPossible (uint256 indexed _disputeID , IArbitrableV2 indexed _arbitrable );
132133 event AppealDecision (uint256 indexed _disputeID , IArbitrableV2 indexed _arbitrable );
@@ -483,12 +484,12 @@ contract KlerosCore is IArbitratorV2 {
483484 /// @param _courtID The ID of the court.
484485 /// @param _stake The new stake.
485486 function setStake (uint96 _courtID , uint256 _stake ) external {
486- if (! _setStakeForAccount (msg .sender , _courtID, _stake, 0 )) revert StakingFailed ();
487+ if (! _setStakeForAccount (msg .sender , _courtID, _stake)) revert StakingFailed ();
487488 }
488489
489- function setStakeBySortitionModule (address _account , uint96 _courtID , uint256 _stake , uint256 _penalty ) external {
490+ function setStakeBySortitionModule (address _account , uint96 _courtID , uint256 _stake ) external {
490491 if (msg .sender != address (sortitionModule)) revert WrongCaller ();
491- _setStakeForAccount (_account, _courtID, _stake, _penalty );
492+ _setStakeForAccount (_account, _courtID, _stake);
492493 }
493494
494495 /// @inheritdoc IArbitratorV2
@@ -614,7 +615,7 @@ contract KlerosCore is IArbitratorV2 {
614615 for (uint256 i = startIndex; i < endIndex; i++ ) {
615616 address drawnAddress = disputeKit.draw (_disputeID);
616617 if (drawnAddress != address (0 )) {
617- jurors[drawnAddress].lockedPnk[dispute.courtID] += round.pnkAtStakePerJuror;
618+ jurors[drawnAddress].lockedPnk += round.pnkAtStakePerJuror;
618619 emit Draw (drawnAddress, _disputeID, currentRound, round.drawnJurors.length );
619620 round.drawnJurors.push (drawnAddress);
620621
@@ -763,16 +764,12 @@ contract KlerosCore is IArbitratorV2 {
763764
764765 // Unlock the PNKs affected by the penalty
765766 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);
767+ jurors[account].lockedPnk -= penalty;
768+
769+ // Apply the penalty to the staked PNKs if there ara any.
770+ // Note that lockedPnk will always cover penalty while stakedPnk can become lower after manual unstaking.
771+ if (jurors[account].stakedPnk >= penalty) {
772+ jurors[account].stakedPnk -= penalty;
776773 }
777774 emit TokenAndETHShift (
778775 account,
@@ -832,10 +829,10 @@ contract KlerosCore is IArbitratorV2 {
832829 uint256 pnkLocked = (round.pnkAtStakePerJuror * degreeOfCoherence) / ALPHA_DIVISOR;
833830
834831 // Release the rest of the PNKs of the juror for this round.
835- jurors[account].lockedPnk[dispute.courtID] -= pnkLocked;
832+ jurors[account].lockedPnk -= pnkLocked;
836833
837834 // Give back the locked PNKs in case the juror fully unstaked earlier.
838- if (jurors[account].stakedPnk[dispute.courtID] == 0 ) {
835+ if (jurors[account].stakedPnk == 0 ) {
839836 pinakion.safeTransfer (account, pnkLocked);
840837 }
841838
@@ -1014,10 +1011,11 @@ contract KlerosCore is IArbitratorV2 {
10141011 function getJurorBalance (
10151012 address _juror ,
10161013 uint96 _courtID
1017- ) external view returns (uint256 staked , uint256 locked , uint256 nbCourts ) {
1014+ ) external view returns (uint256 totalStaked , uint256 totalLocked , uint256 stakedInCourt , uint256 nbCourts ) {
10181015 Juror storage juror = jurors[_juror];
1019- staked = juror.stakedPnk[_courtID];
1020- locked = juror.lockedPnk[_courtID];
1016+ totalStaked = juror.stakedPnk;
1017+ totalLocked = juror.lockedPnk;
1018+ stakedInCourt = juror.stakedPnkByCourt[_courtID];
10211019 nbCourts = juror.courtIDs.length ;
10221020 }
10231021
@@ -1110,35 +1108,33 @@ contract KlerosCore is IArbitratorV2 {
11101108 /// @param _account The address of the juror.
11111109 /// @param _courtID The ID of the court.
11121110 /// @param _stake The new stake.
1113- /// @param _penalty Penalized amount won't be transferred back to juror when the stake is lowered.
11141111 /// @return succeeded True if the call succeeded, false otherwise.
1115- function _setStakeForAccount (
1116- address _account ,
1117- uint96 _courtID ,
1118- uint256 _stake ,
1119- uint256 _penalty
1120- ) internal returns (bool succeeded ) {
1112+ function _setStakeForAccount (address _account , uint96 _courtID , uint256 _stake ) internal returns (bool succeeded ) {
11211113 if (_courtID == FORKING_COURT || _courtID > courts.length ) return false ;
11221114
11231115 Juror storage juror = jurors[_account];
1124- uint256 currentStake = juror.stakedPnk [_courtID];
1116+ uint256 currentStake = juror.stakedPnkByCourt [_courtID];
11251117
11261118 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 ;
1119+ if (_stake < courts[_courtID].minStake) return false ;
11291120 }
11301121
1131- ISortitionModule.preStakeHookResult result = sortitionModule.preStakeHook (_account, _courtID, _stake, _penalty );
1122+ ISortitionModule.preStakeHookResult result = sortitionModule.preStakeHook (_account, _courtID, _stake);
11321123 if (result == ISortitionModule.preStakeHookResult.failed) {
11331124 return false ;
11341125 } else if (result == ISortitionModule.preStakeHookResult.delayed) {
1135- emit StakeDelayed (_account, _courtID, _stake, _penalty );
1126+ emit StakeDelayed (_account, _courtID, _stake);
11361127 return true ;
11371128 }
11381129
11391130 uint256 transferredAmount;
11401131 if (_stake >= currentStake) {
1141- transferredAmount = _stake - currentStake;
1132+ // When stakedPnk becomes lower than lockedPnk count the locked tokens in when transferring tokens from juror.
1133+ // (E.g. stakedPnk = 0, lockedPnk = 150) which can happen if the juror unstaked fully while having some tokens locked.
1134+ uint256 previouslyLocked = (juror.lockedPnk >= juror.stakedPnk) ? juror.lockedPnk - juror.stakedPnk : 0 ;
1135+ transferredAmount = (_stake >= currentStake + previouslyLocked)
1136+ ? _stake - currentStake - previouslyLocked
1137+ : 0 ;
11421138 if (transferredAmount > 0 ) {
11431139 if (pinakion.safeTransferFrom (_account, address (this ), transferredAmount)) {
11441140 if (currentStake == 0 ) {
@@ -1150,8 +1146,14 @@ contract KlerosCore is IArbitratorV2 {
11501146 }
11511147 } else {
11521148 if (_stake == 0 ) {
1153- // Keep locked PNKs in the contract and release them after dispute is executed.
1154- transferredAmount = currentStake - juror.lockedPnk[_courtID] - _penalty;
1149+ // Make sure locked tokens always stay in the contract. They can only be released during Execution.
1150+ if (juror.stakedPnk >= currentStake + juror.lockedPnk) {
1151+ // We have enough pnk staked to afford withdrawal of the current stake while keeping locked tokens.
1152+ transferredAmount = currentStake;
1153+ } else if (juror.stakedPnk >= juror.lockedPnk) {
1154+ // Can't afford withdrawing the current stake fully. Take whatever is available while keeping locked tokens.
1155+ transferredAmount = juror.stakedPnk - juror.lockedPnk;
1156+ }
11551157 if (transferredAmount > 0 ) {
11561158 if (pinakion.safeTransfer (_account, transferredAmount)) {
11571159 for (uint256 i = juror.courtIDs.length ; i > 0 ; i-- ) {
@@ -1166,7 +1168,13 @@ contract KlerosCore is IArbitratorV2 {
11661168 }
11671169 }
11681170 } else {
1169- transferredAmount = currentStake - _stake - _penalty;
1171+ if (juror.stakedPnk >= currentStake - _stake + juror.lockedPnk) {
1172+ // We have enough pnk staked to afford withdrawal while keeping locked tokens.
1173+ transferredAmount = currentStake - _stake;
1174+ } else if (juror.stakedPnk >= juror.lockedPnk) {
1175+ // Can't afford withdrawing the current stake fully. Take whatever is available while keeping locked tokens.
1176+ transferredAmount = juror.stakedPnk - juror.lockedPnk;
1177+ }
11701178 if (transferredAmount > 0 ) {
11711179 if (! pinakion.safeTransfer (_account, transferredAmount)) {
11721180 return false ;
@@ -1175,8 +1183,9 @@ contract KlerosCore is IArbitratorV2 {
11751183 }
11761184 }
11771185
1178- // Update juror's records.
1179- juror.stakedPnk[_courtID] = _stake;
1186+ // Note that stakedPnk can become async with currentStake (e.g. after penalty).
1187+ juror.stakedPnk = (juror.stakedPnk >= currentStake) ? juror.stakedPnk - currentStake + _stake : _stake;
1188+ juror.stakedPnkByCourt[_courtID] = _stake;
11801189
11811190 sortitionModule.setStake (_account, _courtID, _stake);
11821191 emit StakeSet (_account, _courtID, _stake);
0 commit comments