Skip to content

Commit b35a3d0

Browse files
unknownunknown1jaybuidl
authored andcommitted
feat(KC): instant staking
1 parent 4a007e3 commit b35a3d0

File tree

3 files changed

+136
-29
lines changed

3 files changed

+136
-29
lines changed

contracts/src/arbitration/KlerosCore.sol

+77-17
Original file line numberDiff line numberDiff line change
@@ -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);

contracts/src/arbitration/SortitionModule.sol

+53-9
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable {
3939
address account; // The address of the juror.
4040
uint96 courtID; // The ID of the court.
4141
uint256 stake; // The new stake.
42+
bool alreadyTransferred; // True if tokens were already transferred before delayed stake's execution.
4243
}
4344

4445
// ************************************* //
@@ -63,6 +64,7 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable {
6364
uint256 public delayedStakeReadIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped.
6465
mapping(bytes32 => SortitionSumTree) sortitionSumTrees; // The mapping trees by keys.
6566
mapping(uint256 => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking.
67+
mapping(address => mapping(uint96 => uint256)) public latestDelayedStakeIndex; // Maps the juror to its latest delayed stake. If there is already a delayed stake for this juror then it'll be replaced. latestDelayedStakeIndex[juror][courtID].
6668

6769
// ************************************* //
6870
// * Function Modifiers * //
@@ -201,12 +203,40 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable {
201203

202204
for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) {
203205
DelayedStake storage delayedStake = delayedStakes[i];
204-
core.setStakeBySortitionModule(delayedStake.account, delayedStake.courtID, delayedStake.stake);
205-
delete delayedStakes[i];
206+
// Delayed stake could've been manually removed already. In this case simply move on to the next item.
207+
if (delayedStake.account != address(0)) {
208+
core.setStakeBySortitionModule(
209+
delayedStake.account,
210+
delayedStake.courtID,
211+
delayedStake.stake,
212+
delayedStake.alreadyTransferred
213+
);
214+
delete latestDelayedStakeIndex[delayedStake.account][delayedStake.courtID];
215+
delete delayedStakes[i];
216+
}
206217
}
207218
delayedStakeReadIndex = newDelayedStakeReadIndex;
208219
}
209220

221+
/// @dev Checks if there is already a delayed stake. In this case consider it irrelevant and remove it.
222+
/// @param _courtID ID of the court.
223+
/// @param _juror Juror whose stake to check.
224+
function checkExistingDelayedStake(uint96 _courtID, address _juror) external override onlyByCore {
225+
uint256 latestIndex = latestDelayedStakeIndex[_juror][_courtID];
226+
if (latestIndex != 0) {
227+
DelayedStake storage delayedStake = delayedStakes[latestIndex];
228+
if (delayedStake.alreadyTransferred) {
229+
bytes32 stakePathID = _accountAndCourtIDToStakePathID(_juror, _courtID);
230+
// Sortition stake represents the stake value that was last updated during Staking phase.
231+
uint256 sortitionStake = stakeOf(bytes32(uint256(_courtID)), stakePathID);
232+
// Withdraw the tokens that were added with the latest delayed stake.
233+
core.withdrawPartiallyDelayedStake(_courtID, _juror, delayedStake.stake - sortitionStake);
234+
}
235+
delete delayedStakes[latestIndex];
236+
delete latestDelayedStakeIndex[_juror][_courtID];
237+
}
238+
}
239+
210240
function preStakeHook(
211241
address _account,
212242
uint96 _courtID,
@@ -218,12 +248,18 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable {
218248
return preStakeHookResult.failed;
219249
} else {
220250
if (phase != Phase.staking) {
221-
delayedStakes[++delayedStakeWriteIndex] = DelayedStake({
222-
account: _account,
223-
courtID: _courtID,
224-
stake: _stake
225-
});
226-
return preStakeHookResult.delayed;
251+
DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex];
252+
delayedStake.account = _account;
253+
delayedStake.courtID = _courtID;
254+
delayedStake.stake = _stake;
255+
latestDelayedStakeIndex[_account][_courtID] = delayedStakeWriteIndex;
256+
if (_stake > currentStake) {
257+
// Actual token transfer is done right after this hook.
258+
delayedStake.alreadyTransferred = true;
259+
return preStakeHookResult.partiallyDelayed;
260+
} else {
261+
return preStakeHookResult.delayed;
262+
}
227263
}
228264
}
229265
return preStakeHookResult.ok;
@@ -273,7 +309,7 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable {
273309
function setJurorInactive(address _account) external override onlyByCore {
274310
uint96[] memory courtIDs = core.getJurorCourtIDs(_account);
275311
for (uint256 j = courtIDs.length; j > 0; j--) {
276-
core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0);
312+
core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0, false);
277313
}
278314
}
279315

@@ -486,4 +522,12 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable {
486522
stakePathID := mload(ptr)
487523
}
488524
}
525+
526+
function stakeOf(bytes32 _key, bytes32 _ID) public view returns (uint256 value) {
527+
SortitionSumTree storage tree = sortitionSumTrees[_key];
528+
uint treeIndex = tree.IDsToNodeIndexes[_ID];
529+
530+
if (treeIndex == 0) value = 0;
531+
else value = tree.nodes[treeIndex];
532+
}
489533
}

contracts/src/arbitration/interfaces/ISortitionModule.sol

+6-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ interface ISortitionModule {
99
}
1010

1111
enum preStakeHookResult {
12-
ok,
13-
delayed,
14-
failed
12+
ok, // Correct phase. All checks are passed.
13+
partiallyDelayed, // Wrong phase but stake is increased, so transfer the tokens without updating the drawing chance.
14+
delayed, // Wrong phase and stake is decreased. Delay the token transfer and drawing chance update.
15+
failed // Checks didn't pass. Do no changes.
1516
}
1617

1718
event NewPhase(Phase _phase);
@@ -31,4 +32,6 @@ interface ISortitionModule {
3132
function createDisputeHook(uint256 _disputeID, uint256 _roundID) external;
3233

3334
function postDrawHook(uint256 _disputeID, uint256 _roundID) external;
35+
36+
function checkExistingDelayedStake(uint96 _courtID, address _juror) external;
3437
}

0 commit comments

Comments
 (0)