@@ -116,6 +116,7 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
116116
117117 event StakeSet (address indexed _address , uint256 _courtID , uint256 _amount );
118118 event StakeDelayed (address indexed _address , uint256 _courtID , uint256 _amount );
119+ event StakePartiallyDelayed (address indexed _address , uint256 _courtID , uint256 _amount );
119120 event NewPeriod (uint256 indexed _disputeID , Period _period );
120121 event AppealPossible (uint256 indexed _disputeID , IArbitrableV2 indexed _arbitrable );
121122 event AppealDecision (uint256 indexed _disputeID , IArbitrableV2 indexed _arbitrable );
@@ -170,6 +171,7 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
170171 uint256 _feeAmount ,
171172 IERC20 _feeToken
172173 );
174+ event PartiallyDelayedStakeWithdrawn (uint96 indexed _courtID , address indexed _account , uint256 _withdrawnAmount );
173175
174176 // ************************************* //
175177 // * Function Modifiers * //
@@ -456,13 +458,54 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
456458 /// @dev Sets the caller's stake in a court.
457459 /// @param _courtID The ID of the court.
458460 /// @param _newStake The new stake.
461+ /// Note that the existing delayed stake will be nullified as non-relevant.
459462 function setStake (uint96 _courtID , uint256 _newStake ) external {
460- if (! _setStakeForAccount (msg .sender , _courtID, _newStake)) revert StakingFailed ();
463+ removeDelayedStake (_courtID);
464+ if (! _setStakeForAccount (msg .sender , _courtID, _newStake, false )) revert StakingFailed ();
461465 }
462466
463- function setStakeBySortitionModule (address _account , uint96 _courtID , uint256 _newStake ) external {
467+ /// @dev Removes the latest delayed stake if there is any.
468+ /// @param _courtID The ID of the court.
469+ function removeDelayedStake (uint96 _courtID ) public {
470+ sortitionModule.checkExistingDelayedStake (_courtID, msg .sender );
471+ }
472+
473+ function withdrawPartiallyDelayedStake (uint96 _courtID , address _juror , uint256 _amountToWithdraw ) external {
464474 if (msg .sender != address (sortitionModule)) revert WrongCaller ();
465- _setStakeForAccount (_account, _courtID, _newStake);
475+ uint256 actualAmount = _amountToWithdraw;
476+ Juror storage juror = jurors[_juror];
477+ if (juror.stakedPnk <= actualAmount) {
478+ actualAmount = juror.stakedPnk;
479+ }
480+ require (pinakion.safeTransfer (_juror, actualAmount));
481+ // StakePnk can become lower because of penalty, thus we adjust the amount for it. stakedPnkByCourt can't be penalized so subtract the default amount.
482+ juror.stakedPnk -= actualAmount;
483+ juror.stakedPnkByCourt[_courtID] -= _amountToWithdraw;
484+ emit PartiallyDelayedStakeWithdrawn (_courtID, _juror, _amountToWithdraw);
485+ // Note that if we don't delete court here it'll be duplicated after staking.
486+ if (juror.stakedPnkByCourt[_courtID] == 0 ) {
487+ for (uint256 i = juror.courtIDs.length ; i > 0 ; i-- ) {
488+ if (juror.courtIDs[i - 1 ] == _courtID) {
489+ juror.courtIDs[i - 1 ] = juror.courtIDs[juror.courtIDs.length - 1 ];
490+ juror.courtIDs.pop ();
491+ break ;
492+ }
493+ }
494+ }
495+ }
496+
497+ function setStakeBySortitionModule (
498+ address _account ,
499+ uint96 _courtID ,
500+ uint256 _stake ,
501+ bool _alreadyTransferred
502+ ) external {
503+ if (msg .sender != address (sortitionModule)) revert WrongCaller ();
504+ // Always nullify the latest delayed stake before setting a new value.
505+ // Note that we check the delayed stake here too because the check in `setStake` can be bypassed
506+ // if the stake was updated automatically during `execute` (e.g. when unstaking inactive juror).
507+ removeDelayedStake (_courtID);
508+ _setStakeForAccount (_account, _courtID, _newStake, _alreadyTransferred);
466509 }
467510
468511 /// @inheritdoc IArbitratorV2
@@ -1029,11 +1072,17 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
10291072 /// @param _account The address of the juror.
10301073 /// @param _courtID The ID of the court.
10311074 /// @param _newStake The new stake.
1075+ /// @param _alreadyTransferred True if the tokens were already transferred from juror. Only relevant for delayed stakes.
10321076 /// @return succeeded True if the call succeeded, false otherwise.
10331077 function _setStakeForAccount (
1078+
10341079 address _account ,
1080+
10351081 uint96 _courtID ,
1082+
10361083 uint256 _newStake
1084+ ,
1085+ bool _alreadyTransferred
10371086 ) internal returns (bool succeeded ) {
10381087 if (_courtID == Constants.FORKING_COURT || _courtID > courts.length ) return false ;
10391088
@@ -1056,22 +1105,24 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
10561105
10571106 uint256 transferredAmount;
10581107 if (_newStake >= currentStake) {
1059- // Stake increase
1108+ if (! _alreadyTransferred) {
1109+ // Stake increase
10601110 // When stakedPnk becomes lower than lockedPnk count the locked tokens in when transferring tokens from juror.
1061- // (E.g. stakedPnk = 0, lockedPnk = 150) which can happen if the juror unstaked fully while having some tokens locked.
1062- uint256 previouslyLocked = (juror.lockedPnk >= juror.stakedPnk) ? juror.lockedPnk - juror.stakedPnk : 0 ; // underflow guard
1063- transferredAmount = (_newStake >= currentStake + previouslyLocked) // underflow guard
1064- ? _newStake - currentStake - previouslyLocked
1065- : 0 ;
1066- if (transferredAmount > 0 ) {
1067- if (! pinakion.safeTransferFrom (_account, address (this ), transferredAmount)) {
1068- return false ;
1111+ // (E.g. stakedPnk = 0, lockedPnk = 150) which can happen if the juror unstaked fully while having some tokens locked.
1112+ uint256 previouslyLocked = (juror.lockedPnk >= juror.stakedPnk) ? juror.lockedPnk - juror.stakedPnk : 0 ; // underflow guard
1113+ transferredAmount = (_newStake >= currentStake + previouslyLocked) // underflow guard
1114+ ? _newStake - currentStake - previouslyLocked
1115+ : 0 ;
1116+ if (transferredAmount > 0 ) {
1117+ // Note we don't return false after incorrect transfer because when stake is increased the transfer is done immediately, thus it can't disrupt delayed stakes' queue.
1118+ pinakion.safeTransferFrom (_account, address (this ), transferredAmount);
1119+ }
1120+ if (currentStake == 0 ) {
1121+ juror.courtIDs.push (_courtID);
10691122 }
1070- }
1071- if (currentStake == 0 ) {
1072- juror.courtIDs.push (_courtID);
10731123 }
10741124 } else {
1125+ // Note that stakes can be partially delayed only when stake is increased.
10751126 // Stake decrease: make sure locked tokens always stay in the contract. They can only be released during Execution.
10761127 if (juror.stakedPnk >= currentStake - _newStake + juror.lockedPnk) {
10771128 // We have enough pnk staked to afford withdrawal while keeping locked tokens.
@@ -1097,8 +1148,17 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
10971148 }
10981149
10991150 // Note that stakedPnk can become async with currentStake (e.g. after penalty).
1100- juror.stakedPnk = (juror.stakedPnk >= currentStake) ? juror.stakedPnk - currentStake + _newStake : _newStake;
1101- juror.stakedPnkByCourt[_courtID] = _newStake;
1151+ // Also note that these values were already updated if the stake was only partially delayed.
1152+ if (! _alreadyTransferred) {
1153+ juror.stakedPnk = (juror.stakedPnk >= currentStake) ? juror.stakedPnk - currentStake + _newStake : _newStake;
1154+ juror.stakedPnkByCourt[_courtID] = _newStake;
1155+ }
1156+
1157+ // Transfer the tokens but don't update sortition module.
1158+ if (result == ISortitionModule.preStakeHookResult.partiallyDelayed) {
1159+ emit StakePartiallyDelayed (_account, _courtID, _stake);
1160+ return true ;
1161+ }
11021162
11031163 sortitionModule.setStake (_account, _courtID, _newStake);
11041164 emit StakeSet (_account, _courtID, _newStake);
0 commit comments