Skip to content

Commit 17796c1

Browse files
feat(KC): instant staking
1 parent b69abee commit 17796c1

File tree

3 files changed

+112
-53
lines changed

3 files changed

+112
-53
lines changed

contracts/src/arbitration/KlerosCore.sol

+64-35
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ contract KlerosCore is IArbitratorV2 {
127127

128128
event StakeSet(address indexed _address, uint256 _courtID, uint256 _amount);
129129
event StakeDelayed(address indexed _address, uint256 _courtID, uint256 _amount, uint256 _penalty);
130+
event StakePartiallyDelayed(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);
@@ -482,12 +483,28 @@ contract KlerosCore is IArbitratorV2 {
482483
/// @param _courtID The ID of the court.
483484
/// @param _stake The new stake.
484485
function setStake(uint96 _courtID, uint256 _stake) external {
485-
if (!_setStakeForAccount(msg.sender, _courtID, _stake, 0)) revert StakingFailed();
486+
if (!_setStakeForAccount(msg.sender, _courtID, _stake, 0, false)) revert StakingFailed();
486487
}
487488

488-
function setStakeBySortitionModule(address _account, uint96 _courtID, uint256 _stake, uint256 _penalty) external {
489+
function withdrawDelayedStake(uint256 _delayedStakeIndex) external {
490+
// All necessary checks are done in sortition module.
491+
(uint256 amountToWithdraw, uint96 courtID) = sortitionModule.removeDelayedStake(_delayedStakeIndex, msg.sender);
492+
if (jurors[msg.sender].stakedPnk[courtID] <= amountToWithdraw) {
493+
amountToWithdraw = jurors[msg.sender].stakedPnk[courtID];
494+
}
495+
require(pinakion.safeTransfer(msg.sender, amountToWithdraw));
496+
jurors[msg.sender].stakedPnk[courtID] -= amountToWithdraw;
497+
}
498+
499+
function setStakeBySortitionModule(
500+
address _account,
501+
uint96 _courtID,
502+
uint256 _stake,
503+
uint256 _penalty,
504+
bool _alreadyTransferred
505+
) external {
489506
if (msg.sender != address(sortitionModule)) revert WrongCaller();
490-
_setStakeForAccount(_account, _courtID, _stake, _penalty);
507+
_setStakeForAccount(_account, _courtID, _stake, _penalty, _alreadyTransferred);
491508
}
492509

493510
/// @inheritdoc IArbitratorV2
@@ -768,10 +785,11 @@ contract KlerosCore is IArbitratorV2 {
768785
if (jurors[account].stakedPnk[dispute.courtID] >= courts[dispute.courtID].minStake + penalty) {
769786
// The juror still has enough staked PNKs after penalty for this court.
770787
uint256 newStake = jurors[account].stakedPnk[dispute.courtID] - penalty;
771-
_setStakeForAccount(account, dispute.courtID, newStake, penalty);
788+
// `alreadyTransferred` flag can be true only after manual stake increase, which can't happen during penalty.
789+
_setStakeForAccount(account, dispute.courtID, newStake, penalty, false);
772790
} else if (jurors[account].stakedPnk[dispute.courtID] != 0) {
773791
// The juror does not have enough staked PNKs after penalty for this court, unstake them.
774-
_setStakeForAccount(account, dispute.courtID, 0, penalty);
792+
_setStakeForAccount(account, dispute.courtID, 0, penalty, false);
775793
}
776794
emit TokenAndETHShift(
777795
account,
@@ -1110,12 +1128,14 @@ contract KlerosCore is IArbitratorV2 {
11101128
/// @param _courtID The ID of the court.
11111129
/// @param _stake The new stake.
11121130
/// @param _penalty Penalized amount won't be transferred back to juror when the stake is lowered.
1131+
/// @param _alreadyTransferred True if the tokens were already transferred. Only relevant for delayed stake execution.
11131132
/// @return succeeded True if the call succeeded, false otherwise.
11141133
function _setStakeForAccount(
11151134
address _account,
11161135
uint96 _courtID,
11171136
uint256 _stake,
1118-
uint256 _penalty
1137+
uint256 _penalty,
1138+
bool _alreadyTransferred
11191139
) internal returns (bool succeeded) {
11201140
if (_courtID == FORKING_COURT || _courtID > courts.length) return false;
11211141

@@ -1135,47 +1155,56 @@ contract KlerosCore is IArbitratorV2 {
11351155
return true;
11361156
}
11371157

1138-
uint256 transferredAmount;
1139-
if (_stake >= currentStake) {
1140-
transferredAmount = _stake - currentStake;
1141-
if (transferredAmount > 0) {
1142-
if (pinakion.safeTransferFrom(_account, address(this), transferredAmount)) {
1143-
if (currentStake == 0) {
1144-
juror.courtIDs.push(_courtID);
1145-
}
1146-
} else {
1147-
return false;
1148-
}
1149-
}
1150-
} else {
1151-
if (_stake == 0) {
1152-
// Keep locked PNKs in the contract and release them after dispute is executed.
1153-
transferredAmount = currentStake - juror.lockedPnk[_courtID] - _penalty;
1158+
// Don't transfer the tokens and only update the drawing chance if the transfer was already done.
1159+
if (!_alreadyTransferred) {
1160+
uint256 transferredAmount;
1161+
if (_stake >= currentStake) {
1162+
transferredAmount = _stake - currentStake;
11541163
if (transferredAmount > 0) {
1155-
if (pinakion.safeTransfer(_account, transferredAmount)) {
1156-
for (uint256 i = juror.courtIDs.length; i > 0; i--) {
1157-
if (juror.courtIDs[i - 1] == _courtID) {
1158-
juror.courtIDs[i - 1] = juror.courtIDs[juror.courtIDs.length - 1];
1159-
juror.courtIDs.pop();
1160-
break;
1161-
}
1164+
if (pinakion.safeTransferFrom(_account, address(this), transferredAmount)) {
1165+
if (currentStake == 0) {
1166+
juror.courtIDs.push(_courtID);
11621167
}
11631168
} else {
11641169
return false;
11651170
}
11661171
}
11671172
} else {
1168-
transferredAmount = currentStake - _stake - _penalty;
1169-
if (transferredAmount > 0) {
1170-
if (!pinakion.safeTransfer(_account, transferredAmount)) {
1171-
return false;
1173+
if (_stake == 0) {
1174+
// Keep locked PNKs in the contract and release them after dispute is executed.
1175+
transferredAmount = currentStake - juror.lockedPnk[_courtID] - _penalty;
1176+
if (transferredAmount > 0) {
1177+
if (pinakion.safeTransfer(_account, transferredAmount)) {
1178+
for (uint256 i = juror.courtIDs.length; i > 0; i--) {
1179+
if (juror.courtIDs[i - 1] == _courtID) {
1180+
juror.courtIDs[i - 1] = juror.courtIDs[juror.courtIDs.length - 1];
1181+
juror.courtIDs.pop();
1182+
break;
1183+
}
1184+
}
1185+
} else {
1186+
return false;
1187+
}
1188+
}
1189+
} else {
1190+
transferredAmount = currentStake - _stake - _penalty;
1191+
if (transferredAmount > 0) {
1192+
if (!pinakion.safeTransfer(_account, transferredAmount)) {
1193+
return false;
1194+
}
11721195
}
11731196
}
11741197
}
1198+
1199+
// Update juror's records.
1200+
juror.stakedPnk[_courtID] = _stake;
11751201
}
11761202

1177-
// Update juror's records.
1178-
juror.stakedPnk[_courtID] = _stake;
1203+
// Transfer the tokens but don't update sortition module.
1204+
if (result == ISortitionModule.preStakeHookResult.partiallyDelayed) {
1205+
emit StakePartiallyDelayed(_account, _courtID, _stake);
1206+
return true;
1207+
}
11791208

11801209
sortitionModule.setStake(_account, _courtID, _stake);
11811210
emit StakeSet(_account, _courtID, _stake);

contracts/src/arbitration/SortitionModule.sol

+42-15
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ contract SortitionModule is ISortitionModule {
3737
uint96 courtID; // The ID of the court.
3838
uint256 stake; // The new stake.
3939
uint256 penalty; // Penalty value, in case the stake was set during execution.
40+
bool alreadyTransferred; // True if tokens were already transferred before delayed stake's execution.
4041
}
4142

4243
// ************************************* //
@@ -185,17 +186,38 @@ contract SortitionModule is ISortitionModule {
185186

186187
for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) {
187188
DelayedStake storage delayedStake = delayedStakes[i];
188-
core.setStakeBySortitionModule(
189-
delayedStake.account,
190-
delayedStake.courtID,
191-
delayedStake.stake,
192-
delayedStake.penalty
193-
);
194-
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.penalty,
196+
delayedStake.alreadyTransferred
197+
);
198+
delete delayedStakes[i];
199+
}
195200
}
196201
delayedStakeReadIndex = newDelayedStakeReadIndex;
197202
}
198203

204+
/// @dev Remove the delayed stake after its partial execution in order to return the tokens.
205+
/// @param _index Index of the delayed stake to remove.
206+
/// @param _sender Address that attempted removal.
207+
/// @return stake Stake amount that was discarded.
208+
/// @return courtID ID of the court related to delayed stake.
209+
function removeDelayedStake(
210+
uint256 _index,
211+
address _sender
212+
) external override onlyByCore returns (uint256 stake, uint96 courtID) {
213+
DelayedStake storage delayedStake = delayedStakes[_index];
214+
require(delayedStake.account == _sender, "Can only remove your own stake");
215+
require(delayedStake.alreadyTransferred, "No tokens to return");
216+
stake = delayedStake.stake;
217+
courtID = delayedStake.courtID;
218+
delete delayedStakes[_index];
219+
}
220+
199221
function preStakeHook(
200222
address _account,
201223
uint96 _courtID,
@@ -208,13 +230,18 @@ contract SortitionModule is ISortitionModule {
208230
return preStakeHookResult.failed;
209231
} else {
210232
if (phase != Phase.staking) {
211-
delayedStakes[++delayedStakeWriteIndex] = DelayedStake({
212-
account: _account,
213-
courtID: _courtID,
214-
stake: _stake,
215-
penalty: _penalty
216-
});
217-
return preStakeHookResult.delayed;
233+
DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex];
234+
delayedStake.account = _account;
235+
delayedStake.courtID = _courtID;
236+
delayedStake.stake = _stake;
237+
delayedStake.penalty = _penalty;
238+
if (_stake > currentStake) {
239+
// Actual token transfer is done right after this hook.
240+
delayedStake.alreadyTransferred = true;
241+
return preStakeHookResult.partiallyDelayed;
242+
} else {
243+
return preStakeHookResult.delayed;
244+
}
218245
}
219246
}
220247
return preStakeHookResult.ok;
@@ -264,7 +291,7 @@ contract SortitionModule is ISortitionModule {
264291
function setJurorInactive(address _account) external override onlyByCore {
265292
uint96[] memory courtIDs = core.getJurorCourtIDs(_account);
266293
for (uint256 j = courtIDs.length; j > 0; j--) {
267-
core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0, 0);
294+
core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0, 0, false);
268295
}
269296
}
270297

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);
@@ -36,4 +37,6 @@ interface ISortitionModule {
3637
function createDisputeHook(uint256 _disputeID, uint256 _roundID) external;
3738

3839
function postDrawHook(uint256 _disputeID, uint256 _roundID) external;
40+
41+
function removeDelayedStake(uint256 _index, address _sender) external returns (uint256 amount, uint96 courtID);
3942
}

0 commit comments

Comments
 (0)