Skip to content

Commit 308ba85

Browse files
feat(KC): instant staking
1 parent f946f6e commit 308ba85

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
@@ -129,6 +129,7 @@ contract KlerosCore is IArbitratorV2 {
129129

130130
event StakeSet(address indexed _address, uint256 _courtID, uint256 _amount);
131131
event StakeDelayed(address indexed _address, uint256 _courtID, uint256 _amount);
132+
event StakePartiallyDelayed(address indexed _address, uint256 _courtID, uint256 _amount);
132133
event NewPeriod(uint256 indexed _disputeID, Period _period);
133134
event AppealPossible(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable);
134135
event AppealDecision(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable);
@@ -187,6 +188,7 @@ contract KlerosCore is IArbitratorV2 {
187188
uint256 _feeAmount,
188189
IERC20 _feeToken
189190
);
191+
event PartiallyDelayedStakeWithdrawn(uint96 indexed _courtID, address indexed _account, uint256 _withdrawnAmount);
190192

191193
// ************************************* //
192194
// * Function Modifiers * //
@@ -484,13 +486,54 @@ contract KlerosCore is IArbitratorV2 {
484486
/// @dev Sets the caller's stake in a court.
485487
/// @param _courtID The ID of the court.
486488
/// @param _newStake The new stake.
489+
/// Note that the existing delayed stake will be nullified as non-relevant.
487490
function setStake(uint96 _courtID, uint256 _newStake) external {
488-
if (!_setStakeForAccount(msg.sender, _courtID, _newStake)) revert StakingFailed();
491+
removeDelayedStake(_courtID);
492+
if (!_setStakeForAccount(msg.sender, _courtID, _newStake, false)) revert StakingFailed();
489493
}
490494

491-
function setStakeBySortitionModule(address _account, uint96 _courtID, uint256 _newStake) external {
495+
/// @dev Removes the latest delayed stake if there is any.
496+
/// @param _courtID The ID of the court.
497+
function removeDelayedStake(uint96 _courtID) public {
498+
sortitionModule.checkExistingDelayedStake(_courtID, msg.sender);
499+
}
500+
501+
function withdrawPartiallyDelayedStake(uint96 _courtID, address _juror, uint256 _amountToWithdraw) external {
502+
if (msg.sender != address(sortitionModule)) revert WrongCaller();
503+
uint256 actualAmount = _amountToWithdraw;
504+
Juror storage juror = jurors[_juror];
505+
if (juror.stakedPnk <= actualAmount) {
506+
actualAmount = juror.stakedPnk;
507+
}
508+
require(pinakion.safeTransfer(_juror, actualAmount));
509+
// StakePnk can become lower because of penalty, thus we adjust the amount for it. stakedPnkByCourt can't be penalized so subtract the default amount.
510+
juror.stakedPnk -= actualAmount;
511+
juror.stakedPnkByCourt[_courtID] -= _amountToWithdraw;
512+
emit PartiallyDelayedStakeWithdrawn(_courtID, _juror, _amountToWithdraw);
513+
// Note that if we don't delete court here it'll be duplicated after staking.
514+
if (juror.stakedPnkByCourt[_courtID] == 0) {
515+
for (uint256 i = juror.courtIDs.length; i > 0; i--) {
516+
if (juror.courtIDs[i - 1] == _courtID) {
517+
juror.courtIDs[i - 1] = juror.courtIDs[juror.courtIDs.length - 1];
518+
juror.courtIDs.pop();
519+
break;
520+
}
521+
}
522+
}
523+
}
524+
525+
function setStakeBySortitionModule(
526+
address _account,
527+
uint96 _courtID,
528+
uint256 _stake,
529+
bool _alreadyTransferred
530+
) external {
492531
if (msg.sender != address(sortitionModule)) revert WrongCaller();
493-
_setStakeForAccount(_account, _courtID, _newStake);
532+
// Always nullify the latest delayed stake before setting a new value.
533+
// Note that we check the delayed stake here too because the check in `setStake` can be bypassed
534+
// if the stake was updated automatically during `execute` (e.g. when unstaking inactive juror).
535+
removeDelayedStake(_courtID);
536+
_setStakeForAccount(_account, _courtID, _newStake, _alreadyTransferred);
494537
}
495538

496539
/// @inheritdoc IArbitratorV2
@@ -1081,11 +1124,17 @@ contract KlerosCore is IArbitratorV2 {
10811124
/// @param _account The address of the juror.
10821125
/// @param _courtID The ID of the court.
10831126
/// @param _newStake The new stake.
1127+
/// @param _alreadyTransferred True if the tokens were already transferred from juror. Only relevant for delayed stakes.
10841128
/// @return succeeded True if the call succeeded, false otherwise.
10851129
function _setStakeForAccount(
1130+
10861131
address _account,
1132+
10871133
uint96 _courtID,
1134+
10881135
uint256 _newStake
1136+
,
1137+
bool _alreadyTransferred
10891138
) internal returns (bool succeeded) {
10901139
if (_courtID == FORKING_COURT || _courtID > courts.length) return false;
10911140

@@ -1108,22 +1157,24 @@ contract KlerosCore is IArbitratorV2 {
11081157

11091158
uint256 transferredAmount;
11101159
if (_newStake >= currentStake) {
1111-
// Stake increase
1160+
if (!_alreadyTransferred) {
1161+
// Stake increase
11121162
// 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;
1118-
if (transferredAmount > 0) {
1119-
if (!pinakion.safeTransferFrom(_account, address(this), transferredAmount)) {
1120-
return false;
1163+
// (E.g. stakedPnk = 0, lockedPnk = 150) which can happen if the juror unstaked fully while having some tokens locked.
1164+
uint256 previouslyLocked = (juror.lockedPnk >= juror.stakedPnk) ? juror.lockedPnk - juror.stakedPnk : 0; // underflow guard
1165+
transferredAmount = (_newStake >= currentStake + previouslyLocked) // underflow guard
1166+
? _newStake - currentStake - previouslyLocked
1167+
: 0;
1168+
if (transferredAmount > 0) {
1169+
// 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.
1170+
pinakion.safeTransferFrom(_account, address(this), transferredAmount);
1171+
}
1172+
if (currentStake == 0) {
1173+
juror.courtIDs.push(_courtID);
11211174
}
1122-
}
1123-
if (currentStake == 0) {
1124-
juror.courtIDs.push(_courtID);
11251175
}
11261176
} else {
1177+
// Note that stakes can be partially delayed only when stake is increased.
11271178
// Stake decrease: make sure locked tokens always stay in the contract. They can only be released during Execution.
11281179
if (juror.stakedPnk >= currentStake - _newStake + juror.lockedPnk) {
11291180
// We have enough pnk staked to afford withdrawal while keeping locked tokens.
@@ -1149,8 +1200,17 @@ contract KlerosCore is IArbitratorV2 {
11491200
}
11501201

11511202
// 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;
1203+
// Also note that these values were already updated if the stake was only partially delayed.
1204+
if (!_alreadyTransferred) {
1205+
juror.stakedPnk = (juror.stakedPnk >= currentStake) ? juror.stakedPnk - currentStake + _newStake : _newStake;
1206+
juror.stakedPnkByCourt[_courtID] = _newStake;
1207+
}
1208+
1209+
// Transfer the tokens but don't update sortition module.
1210+
if (result == ISortitionModule.preStakeHookResult.partiallyDelayed) {
1211+
emit StakePartiallyDelayed(_account, _courtID, _stake);
1212+
return true;
1213+
}
11541214

11551215
sortitionModule.setStake(_account, _courtID, _newStake);
11561216
emit StakeSet(_account, _courtID, _newStake);

contracts/src/arbitration/SortitionModule.sol

+53-9
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ contract SortitionModule is ISortitionModule {
3636
address account; // The address of the juror.
3737
uint96 courtID; // The ID of the court.
3838
uint256 stake; // The new stake.
39+
bool alreadyTransferred; // True if tokens were already transferred before delayed stake's execution.
3940
}
4041

4142
// ************************************* //
@@ -60,6 +61,7 @@ contract SortitionModule is ISortitionModule {
6061
uint256 public delayedStakeReadIndex = 1; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped.
6162
mapping(bytes32 => SortitionSumTree) sortitionSumTrees; // The mapping trees by keys.
6263
mapping(uint256 => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking.
64+
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].
6365

6466
// ************************************* //
6567
// * Function Modifiers * //
@@ -184,12 +186,40 @@ contract SortitionModule is ISortitionModule {
184186

185187
for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) {
186188
DelayedStake storage delayedStake = delayedStakes[i];
187-
core.setStakeBySortitionModule(delayedStake.account, delayedStake.courtID, delayedStake.stake);
188-
delete delayedStakes[i];
189+
// Delayed stake could've been manually removed already. In this case simply move on to the next item.
190+
if (delayedStake.account != address(0)) {
191+
core.setStakeBySortitionModule(
192+
delayedStake.account,
193+
delayedStake.courtID,
194+
delayedStake.stake,
195+
delayedStake.alreadyTransferred
196+
);
197+
delete latestDelayedStakeIndex[delayedStake.account][delayedStake.courtID];
198+
delete delayedStakes[i];
199+
}
189200
}
190201
delayedStakeReadIndex = newDelayedStakeReadIndex;
191202
}
192203

204+
/// @dev Checks if there is already a delayed stake. In this case consider it irrelevant and remove it.
205+
/// @param _courtID ID of the court.
206+
/// @param _juror Juror whose stake to check.
207+
function checkExistingDelayedStake(uint96 _courtID, address _juror) external override onlyByCore {
208+
uint256 latestIndex = latestDelayedStakeIndex[_juror][_courtID];
209+
if (latestIndex != 0) {
210+
DelayedStake storage delayedStake = delayedStakes[latestIndex];
211+
if (delayedStake.alreadyTransferred) {
212+
bytes32 stakePathID = _accountAndCourtIDToStakePathID(_juror, _courtID);
213+
// Sortition stake represents the stake value that was last updated during Staking phase.
214+
uint256 sortitionStake = stakeOf(bytes32(uint256(_courtID)), stakePathID);
215+
// Withdraw the tokens that were added with the latest delayed stake.
216+
core.withdrawPartiallyDelayedStake(_courtID, _juror, delayedStake.stake - sortitionStake);
217+
}
218+
delete delayedStakes[latestIndex];
219+
delete latestDelayedStakeIndex[_juror][_courtID];
220+
}
221+
}
222+
193223
function preStakeHook(
194224
address _account,
195225
uint96 _courtID,
@@ -201,12 +231,18 @@ contract SortitionModule is ISortitionModule {
201231
return preStakeHookResult.failed;
202232
} else {
203233
if (phase != Phase.staking) {
204-
delayedStakes[++delayedStakeWriteIndex] = DelayedStake({
205-
account: _account,
206-
courtID: _courtID,
207-
stake: _stake
208-
});
209-
return preStakeHookResult.delayed;
234+
DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex];
235+
delayedStake.account = _account;
236+
delayedStake.courtID = _courtID;
237+
delayedStake.stake = _stake;
238+
latestDelayedStakeIndex[_account][_courtID] = delayedStakeWriteIndex;
239+
if (_stake > currentStake) {
240+
// Actual token transfer is done right after this hook.
241+
delayedStake.alreadyTransferred = true;
242+
return preStakeHookResult.partiallyDelayed;
243+
} else {
244+
return preStakeHookResult.delayed;
245+
}
210246
}
211247
}
212248
return preStakeHookResult.ok;
@@ -256,7 +292,7 @@ contract SortitionModule is ISortitionModule {
256292
function setJurorInactive(address _account) external override onlyByCore {
257293
uint96[] memory courtIDs = core.getJurorCourtIDs(_account);
258294
for (uint256 j = courtIDs.length; j > 0; j--) {
259-
core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0);
295+
core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0, false);
260296
}
261297
}
262298

@@ -469,4 +505,12 @@ contract SortitionModule is ISortitionModule {
469505
stakePathID := mload(ptr)
470506
}
471507
}
508+
509+
function stakeOf(bytes32 _key, bytes32 _ID) public view returns (uint256 value) {
510+
SortitionSumTree storage tree = sortitionSumTrees[_key];
511+
uint treeIndex = tree.IDsToNodeIndexes[_ID];
512+
513+
if (treeIndex == 0) value = 0;
514+
else value = tree.nodes[treeIndex];
515+
}
472516
}

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)