@@ -116,6 +116,7 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
116
116
117
117
event StakeSet (address indexed _address , uint256 _courtID , uint256 _amount );
118
118
event StakeDelayed (address indexed _address , uint256 _courtID , uint256 _amount );
119
+ event StakePartiallyDelayed (address indexed _address , uint256 _courtID , uint256 _amount );
119
120
event NewPeriod (uint256 indexed _disputeID , Period _period );
120
121
event AppealPossible (uint256 indexed _disputeID , IArbitrableV2 indexed _arbitrable );
121
122
event AppealDecision (uint256 indexed _disputeID , IArbitrableV2 indexed _arbitrable );
@@ -170,6 +171,7 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
170
171
uint256 _feeAmount ,
171
172
IERC20 _feeToken
172
173
);
174
+ event PartiallyDelayedStakeWithdrawn (uint96 indexed _courtID , address indexed _account , uint256 _withdrawnAmount );
173
175
174
176
// ************************************* //
175
177
// * Function Modifiers * //
@@ -456,13 +458,54 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
456
458
/// @dev Sets the caller's stake in a court.
457
459
/// @param _courtID The ID of the court.
458
460
/// @param _newStake The new stake.
461
+ /// Note that the existing delayed stake will be nullified as non-relevant.
459
462
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 ();
461
465
}
462
466
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 {
464
474
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);
466
509
}
467
510
468
511
/// @inheritdoc IArbitratorV2
@@ -1029,11 +1072,17 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
1029
1072
/// @param _account The address of the juror.
1030
1073
/// @param _courtID The ID of the court.
1031
1074
/// @param _newStake The new stake.
1075
+ /// @param _alreadyTransferred True if the tokens were already transferred from juror. Only relevant for delayed stakes.
1032
1076
/// @return succeeded True if the call succeeded, false otherwise.
1033
1077
function _setStakeForAccount (
1078
+
1034
1079
address _account ,
1080
+
1035
1081
uint96 _courtID ,
1082
+
1036
1083
uint256 _newStake
1084
+ ,
1085
+ bool _alreadyTransferred
1037
1086
) internal returns (bool succeeded ) {
1038
1087
if (_courtID == Constants.FORKING_COURT || _courtID > courts.length ) return false ;
1039
1088
@@ -1056,22 +1105,24 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
1056
1105
1057
1106
uint256 transferredAmount;
1058
1107
if (_newStake >= currentStake) {
1059
- // Stake increase
1108
+ if (! _alreadyTransferred) {
1109
+ // Stake increase
1060
1110
// 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);
1069
1122
}
1070
- }
1071
- if (currentStake == 0 ) {
1072
- juror.courtIDs.push (_courtID);
1073
1123
}
1074
1124
} else {
1125
+ // Note that stakes can be partially delayed only when stake is increased.
1075
1126
// Stake decrease: make sure locked tokens always stay in the contract. They can only be released during Execution.
1076
1127
if (juror.stakedPnk >= currentStake - _newStake + juror.lockedPnk) {
1077
1128
// We have enough pnk staked to afford withdrawal while keeping locked tokens.
@@ -1097,8 +1148,17 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
1097
1148
}
1098
1149
1099
1150
// 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
+ }
1102
1162
1103
1163
sortitionModule.setStake (_account, _courtID, _newStake);
1104
1164
emit StakeSet (_account, _courtID, _newStake);
0 commit comments