Skip to content

Commit 73782d9

Browse files
unknownunknown1jaybuidl
authored andcommitted
fix(KlerosCore): staking logic fix
1 parent 8cb7245 commit 73782d9

File tree

5 files changed

+65
-64
lines changed

5 files changed

+65
-64
lines changed

Diff for: contracts/src/arbitration/KlerosCore.sol

+49-40
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,9 @@ contract KlerosCore is IArbitratorV2 {
6868

6969
struct Juror {
7070
uint96[] courtIDs; // The IDs of courts where the juror's stake path ends. A stake path is a path from the general court to a court the juror directly staked in using `_setStake`.
71-
mapping(uint96 => uint256) stakedPnk; // The amount of PNKs the juror has staked in the court in the form `stakedPnk[courtID]`.
72-
mapping(uint96 => uint256) lockedPnk; // The amount of PNKs the juror has locked in the court in the form `lockedPnk[courtID]`.
71+
uint256 stakedPnk; // The juror's total amount of tokens staked in subcourts. Reflects actual pnk balance.
72+
uint256 lockedPnk; // The juror's total amount of tokens locked in disputes. Can reflect actual pnk balance when stakedPnk are fully withdrawn.
73+
mapping(uint96 => uint256) stakedPnkByCourt; // The amount of PNKs the juror has staked in the court in the form `stakedPnkByCourt[courtID]`.
7374
}
7475

7576
struct DisputeKitNode {
@@ -126,7 +127,7 @@ contract KlerosCore is IArbitratorV2 {
126127
// ************************************* //
127128

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

489-
function setStakeBySortitionModule(address _account, uint96 _courtID, uint256 _stake, uint256 _penalty) external {
490+
function setStakeBySortitionModule(address _account, uint96 _courtID, uint256 _stake) external {
490491
if (msg.sender != address(sortitionModule)) revert WrongCaller();
491-
_setStakeForAccount(_account, _courtID, _stake, _penalty);
492+
_setStakeForAccount(_account, _courtID, _stake);
492493
}
493494

494495
/// @inheritdoc IArbitratorV2
@@ -614,7 +615,7 @@ contract KlerosCore is IArbitratorV2 {
614615
for (uint256 i = startIndex; i < endIndex; i++) {
615616
address drawnAddress = disputeKit.draw(_disputeID);
616617
if (drawnAddress != address(0)) {
617-
jurors[drawnAddress].lockedPnk[dispute.courtID] += round.pnkAtStakePerJuror;
618+
jurors[drawnAddress].lockedPnk += round.pnkAtStakePerJuror;
618619
emit Draw(drawnAddress, _disputeID, currentRound, round.drawnJurors.length);
619620
round.drawnJurors.push(drawnAddress);
620621

@@ -763,16 +764,12 @@ contract KlerosCore is IArbitratorV2 {
763764

764765
// Unlock the PNKs affected by the penalty
765766
address account = round.drawnJurors[_params.repartition];
766-
jurors[account].lockedPnk[dispute.courtID] -= penalty;
767-
768-
// Apply the penalty to the staked PNKs
769-
if (jurors[account].stakedPnk[dispute.courtID] >= courts[dispute.courtID].minStake + penalty) {
770-
// The juror still has enough staked PNKs after penalty for this court.
771-
uint256 newStake = jurors[account].stakedPnk[dispute.courtID] - penalty;
772-
_setStakeForAccount(account, dispute.courtID, newStake, penalty);
773-
} else if (jurors[account].stakedPnk[dispute.courtID] != 0) {
774-
// The juror does not have enough staked PNKs after penalty for this court, unstake them.
775-
_setStakeForAccount(account, dispute.courtID, 0, penalty);
767+
jurors[account].lockedPnk -= penalty;
768+
769+
// Apply the penalty to the staked PNKs if there ara any.
770+
// Note that lockedPnk will always cover penalty while stakedPnk can become lower after manual unstaking.
771+
if (jurors[account].stakedPnk >= penalty) {
772+
jurors[account].stakedPnk -= penalty;
776773
}
777774
emit TokenAndETHShift(
778775
account,
@@ -832,10 +829,10 @@ contract KlerosCore is IArbitratorV2 {
832829
uint256 pnkLocked = (round.pnkAtStakePerJuror * degreeOfCoherence) / ALPHA_DIVISOR;
833830

834831
// Release the rest of the PNKs of the juror for this round.
835-
jurors[account].lockedPnk[dispute.courtID] -= pnkLocked;
832+
jurors[account].lockedPnk -= pnkLocked;
836833

837834
// Give back the locked PNKs in case the juror fully unstaked earlier.
838-
if (jurors[account].stakedPnk[dispute.courtID] == 0) {
835+
if (jurors[account].stakedPnk == 0) {
839836
pinakion.safeTransfer(account, pnkLocked);
840837
}
841838

@@ -1014,10 +1011,11 @@ contract KlerosCore is IArbitratorV2 {
10141011
function getJurorBalance(
10151012
address _juror,
10161013
uint96 _courtID
1017-
) external view returns (uint256 staked, uint256 locked, uint256 nbCourts) {
1014+
) external view returns (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) {
10181015
Juror storage juror = jurors[_juror];
1019-
staked = juror.stakedPnk[_courtID];
1020-
locked = juror.lockedPnk[_courtID];
1016+
totalStaked = juror.stakedPnk;
1017+
totalLocked = juror.lockedPnk;
1018+
stakedInCourt = juror.stakedPnkByCourt[_courtID];
10211019
nbCourts = juror.courtIDs.length;
10221020
}
10231021

@@ -1110,35 +1108,33 @@ contract KlerosCore is IArbitratorV2 {
11101108
/// @param _account The address of the juror.
11111109
/// @param _courtID The ID of the court.
11121110
/// @param _stake The new stake.
1113-
/// @param _penalty Penalized amount won't be transferred back to juror when the stake is lowered.
11141111
/// @return succeeded True if the call succeeded, false otherwise.
1115-
function _setStakeForAccount(
1116-
address _account,
1117-
uint96 _courtID,
1118-
uint256 _stake,
1119-
uint256 _penalty
1120-
) internal returns (bool succeeded) {
1112+
function _setStakeForAccount(address _account, uint96 _courtID, uint256 _stake) internal returns (bool succeeded) {
11211113
if (_courtID == FORKING_COURT || _courtID > courts.length) return false;
11221114

11231115
Juror storage juror = jurors[_account];
1124-
uint256 currentStake = juror.stakedPnk[_courtID];
1116+
uint256 currentStake = juror.stakedPnkByCourt[_courtID];
11251117

11261118
if (_stake != 0) {
1127-
// Check against locked PNKs in case the min stake was lowered.
1128-
if (_stake < courts[_courtID].minStake || _stake < juror.lockedPnk[_courtID]) return false;
1119+
if (_stake < courts[_courtID].minStake) return false;
11291120
}
11301121

1131-
ISortitionModule.preStakeHookResult result = sortitionModule.preStakeHook(_account, _courtID, _stake, _penalty);
1122+
ISortitionModule.preStakeHookResult result = sortitionModule.preStakeHook(_account, _courtID, _stake);
11321123
if (result == ISortitionModule.preStakeHookResult.failed) {
11331124
return false;
11341125
} else if (result == ISortitionModule.preStakeHookResult.delayed) {
1135-
emit StakeDelayed(_account, _courtID, _stake, _penalty);
1126+
emit StakeDelayed(_account, _courtID, _stake);
11361127
return true;
11371128
}
11381129

11391130
uint256 transferredAmount;
11401131
if (_stake >= currentStake) {
1141-
transferredAmount = _stake - currentStake;
1132+
// When stakedPnk becomes lower than lockedPnk count the locked tokens in when transferring tokens from juror.
1133+
// (E.g. stakedPnk = 0, lockedPnk = 150) which can happen if the juror unstaked fully while having some tokens locked.
1134+
uint256 previouslyLocked = (juror.lockedPnk >= juror.stakedPnk) ? juror.lockedPnk - juror.stakedPnk : 0;
1135+
transferredAmount = (_stake >= currentStake + previouslyLocked)
1136+
? _stake - currentStake - previouslyLocked
1137+
: 0;
11421138
if (transferredAmount > 0) {
11431139
if (pinakion.safeTransferFrom(_account, address(this), transferredAmount)) {
11441140
if (currentStake == 0) {
@@ -1150,8 +1146,14 @@ contract KlerosCore is IArbitratorV2 {
11501146
}
11511147
} else {
11521148
if (_stake == 0) {
1153-
// Keep locked PNKs in the contract and release them after dispute is executed.
1154-
transferredAmount = currentStake - juror.lockedPnk[_courtID] - _penalty;
1149+
// Make sure locked tokens always stay in the contract. They can only be released during Execution.
1150+
if (juror.stakedPnk >= currentStake + juror.lockedPnk) {
1151+
// We have enough pnk staked to afford withdrawal of the current stake while keeping locked tokens.
1152+
transferredAmount = currentStake;
1153+
} else if (juror.stakedPnk >= juror.lockedPnk) {
1154+
// Can't afford withdrawing the current stake fully. Take whatever is available while keeping locked tokens.
1155+
transferredAmount = juror.stakedPnk - juror.lockedPnk;
1156+
}
11551157
if (transferredAmount > 0) {
11561158
if (pinakion.safeTransfer(_account, transferredAmount)) {
11571159
for (uint256 i = juror.courtIDs.length; i > 0; i--) {
@@ -1166,7 +1168,13 @@ contract KlerosCore is IArbitratorV2 {
11661168
}
11671169
}
11681170
} else {
1169-
transferredAmount = currentStake - _stake - _penalty;
1171+
if (juror.stakedPnk >= currentStake - _stake + juror.lockedPnk) {
1172+
// We have enough pnk staked to afford withdrawal while keeping locked tokens.
1173+
transferredAmount = currentStake - _stake;
1174+
} else if (juror.stakedPnk >= juror.lockedPnk) {
1175+
// Can't afford withdrawing the current stake fully. Take whatever is available while keeping locked tokens.
1176+
transferredAmount = juror.stakedPnk - juror.lockedPnk;
1177+
}
11701178
if (transferredAmount > 0) {
11711179
if (!pinakion.safeTransfer(_account, transferredAmount)) {
11721180
return false;
@@ -1175,8 +1183,9 @@ contract KlerosCore is IArbitratorV2 {
11751183
}
11761184
}
11771185

1178-
// Update juror's records.
1179-
juror.stakedPnk[_courtID] = _stake;
1186+
// Note that stakedPnk can become async with currentStake (e.g. after penalty).
1187+
juror.stakedPnk = (juror.stakedPnk >= currentStake) ? juror.stakedPnk - currentStake + _stake : _stake;
1188+
juror.stakedPnkByCourt[_courtID] = _stake;
11801189

11811190
sortitionModule.setStake(_account, _courtID, _stake);
11821191
emit StakeSet(_account, _courtID, _stake);

Diff for: contracts/src/arbitration/SortitionModule.sol

+5-13
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ 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-
uint256 penalty; // Penalty value, in case the stake was set during execution.
4039
}
4140

4241
// ************************************* //
@@ -185,12 +184,7 @@ contract SortitionModule is ISortitionModule {
185184

186185
for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) {
187186
DelayedStake storage delayedStake = delayedStakes[i];
188-
core.setStakeBySortitionModule(
189-
delayedStake.account,
190-
delayedStake.courtID,
191-
delayedStake.stake,
192-
delayedStake.penalty
193-
);
187+
core.setStakeBySortitionModule(delayedStake.account, delayedStake.courtID, delayedStake.stake);
194188
delete delayedStakes[i];
195189
}
196190
delayedStakeReadIndex = newDelayedStakeReadIndex;
@@ -199,10 +193,9 @@ contract SortitionModule is ISortitionModule {
199193
function preStakeHook(
200194
address _account,
201195
uint96 _courtID,
202-
uint256 _stake,
203-
uint256 _penalty
196+
uint256 _stake
204197
) external override onlyByCore returns (preStakeHookResult) {
205-
(uint256 currentStake, , uint256 nbCourts) = core.getJurorBalance(_account, _courtID);
198+
(, , uint256 currentStake, uint256 nbCourts) = core.getJurorBalance(_account, _courtID);
206199
if (currentStake == 0 && nbCourts >= MAX_STAKE_PATHS) {
207200
// Prevent staking beyond MAX_STAKE_PATHS but unstaking is always allowed.
208201
return preStakeHookResult.failed;
@@ -211,8 +204,7 @@ contract SortitionModule is ISortitionModule {
211204
delayedStakes[++delayedStakeWriteIndex] = DelayedStake({
212205
account: _account,
213206
courtID: _courtID,
214-
stake: _stake,
215-
penalty: _penalty
207+
stake: _stake
216208
});
217209
return preStakeHookResult.delayed;
218210
}
@@ -264,7 +256,7 @@ contract SortitionModule is ISortitionModule {
264256
function setJurorInactive(address _account) external override onlyByCore {
265257
uint96[] memory courtIDs = core.getJurorCourtIDs(_account);
266258
for (uint256 j = courtIDs.length; j > 0; j--) {
267-
core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0, 0);
259+
core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0);
268260
}
269261
}
270262

Diff for: contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol

+8-2
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,13 @@ contract DisputeKitClassic is BaseDisputeKit, IEvidence {
558558
// ************************************* //
559559

560560
/// @inheritdoc BaseDisputeKit
561-
function _postDrawCheck(uint256 /*_coreDisputeID*/, address /*_juror*/) internal pure override returns (bool) {
562-
return true;
561+
function _postDrawCheck(uint256 _coreDisputeID, address _juror) internal view override returns (bool) {
562+
(uint96 courtID, , , , ) = core.disputes(_coreDisputeID);
563+
(, uint256 lockedAmountPerJuror, , , , , , , , ) = core.getRoundInfo(
564+
_coreDisputeID,
565+
core.getNumberOfRounds(_coreDisputeID) - 1
566+
);
567+
(uint256 totalStaked, uint256 totalLocked, , ) = core.getJurorBalance(_juror, courtID);
568+
return totalStaked >= totalLocked + lockedAmountPerJuror;
563569
}
564570
}

Diff for: contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol

+2-3
Original file line numberDiff line numberDiff line change
@@ -587,9 +587,8 @@ contract DisputeKitSybilResistant is BaseDisputeKit, IEvidence {
587587
_coreDisputeID,
588588
core.getNumberOfRounds(_coreDisputeID) - 1
589589
);
590-
(uint256 staked, uint256 locked, ) = core.getJurorBalance(_juror, courtID);
591-
(, , uint256 minStake, , , , ) = core.courts(courtID);
592-
if (staked < locked + lockedAmountPerJuror || staked < minStake) {
590+
(uint256 totalStaked, uint256 totalLocked, , ) = core.getJurorBalance(_juror, courtID);
591+
if (totalStaked < totalLocked + lockedAmountPerJuror) {
593592
return false;
594593
} else {
595594
return _proofOfHumanity(_juror);

Diff for: contracts/src/arbitration/interfaces/ISortitionModule.sol

+1-6
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,7 @@ interface ISortitionModule {
2626

2727
function draw(bytes32 _court, uint256 _coreDisputeID, uint256 _voteID) external view returns (address);
2828

29-
function preStakeHook(
30-
address _account,
31-
uint96 _courtID,
32-
uint256 _stake,
33-
uint256 _penalty
34-
) external returns (preStakeHookResult);
29+
function preStakeHook(address _account, uint96 _courtID, uint256 _stake) external returns (preStakeHookResult);
3530

3631
function createDisputeHook(uint256 _disputeID, uint256 _roundID) external;
3732

0 commit comments

Comments
 (0)