Skip to content

Commit 17502d9

Browse files
authored
Merge pull request #1228 from unknownunknown1/feat/instant-staking
Instant staking
2 parents af4323b + 6cbe8c4 commit 17502d9

File tree

13 files changed

+635
-276
lines changed

13 files changed

+635
-276
lines changed

contracts/.mocharc.json

-4
This file was deleted.

contracts/hardhat.config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,9 @@ const config: HardhatUserConfig = {
302302
clear: true,
303303
runOnCompile: false,
304304
},
305+
mocha: {
306+
timeout: 20000,
307+
},
305308
tenderly: {
306309
project: process.env.TENDERLY_PROJECT !== undefined ? process.env.TENDERLY_PROJECT : "kleros-v2",
307310
username: process.env.TENDERLY_USERNAME !== undefined ? process.env.TENDERLY_USERNAME : "",

contracts/src/arbitration/KlerosCore.sol

+39-120
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,6 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
7070
uint256 drawIterations; // The number of iterations passed drawing the jurors for this round.
7171
}
7272

73-
struct Juror {
74-
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`.
75-
uint256 stakedPnk; // The juror's total amount of tokens staked in subcourts. Reflects actual pnk balance.
76-
uint256 lockedPnk; // The juror's total amount of tokens locked in disputes. Can reflect actual pnk balance when stakedPnk are fully withdrawn.
77-
mapping(uint96 => uint256) stakedPnkByCourt; // The amount of PNKs the juror has staked in the court in the form `stakedPnkByCourt[courtID]`.
78-
}
79-
8073
// Workaround "stack too deep" errors
8174
struct ExecuteParams {
8275
uint256 disputeID; // The ID of the dispute to execute.
@@ -107,15 +100,12 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
107100
Court[] public courts; // The courts.
108101
IDisputeKit[] public disputeKits; // Array of dispute kits.
109102
Dispute[] public disputes; // The disputes.
110-
mapping(address => Juror) internal jurors; // The jurors.
111103
mapping(IERC20 => CurrencyRate) public currencyRates; // The price of each token in ETH.
112104

113105
// ************************************* //
114106
// * Events * //
115107
// ************************************* //
116108

117-
event StakeSet(address indexed _address, uint256 _courtID, uint256 _amount);
118-
event StakeDelayed(address indexed _address, uint256 _courtID, uint256 _amount);
119109
event NewPeriod(uint256 indexed _disputeID, Period _period);
120110
event AppealPossible(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable);
121111
event AppealDecision(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable);
@@ -456,13 +446,19 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
456446
/// @dev Sets the caller's stake in a court.
457447
/// @param _courtID The ID of the court.
458448
/// @param _newStake The new stake.
449+
/// Note that the existing delayed stake will be nullified as non-relevant.
459450
function setStake(uint96 _courtID, uint256 _newStake) external {
460-
if (!_setStakeForAccount(msg.sender, _courtID, _newStake)) revert StakingFailed();
451+
_setStake(msg.sender, _courtID, _newStake, false);
461452
}
462453

463-
function setStakeBySortitionModule(address _account, uint96 _courtID, uint256 _newStake) external {
464-
if (msg.sender != address(sortitionModule)) revert WrongCaller();
465-
_setStakeForAccount(_account, _courtID, _newStake);
454+
function setStakeBySortitionModule(
455+
address _account,
456+
uint96 _courtID,
457+
uint256 _newStake,
458+
bool _alreadyTransferred
459+
) external {
460+
if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly();
461+
_setStake(_account, _courtID, _newStake, _alreadyTransferred);
466462
}
467463

468464
/// @inheritdoc IArbitratorV2
@@ -589,7 +585,7 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
589585
if (drawnAddress == address(0)) {
590586
continue;
591587
}
592-
jurors[drawnAddress].lockedPnk += round.pnkAtStakePerJuror;
588+
sortitionModule.lockStake(drawnAddress, round.pnkAtStakePerJuror);
593589
emit Draw(drawnAddress, _disputeID, currentRound, round.drawnJurors.length);
594590
round.drawnJurors.push(drawnAddress);
595591
if (round.drawnJurors.length == round.nbVotes) {
@@ -728,15 +724,10 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
728724

729725
// Unlock the PNKs affected by the penalty
730726
address account = round.drawnJurors[_params.repartition];
731-
jurors[account].lockedPnk -= penalty;
727+
sortitionModule.unlockStake(account, penalty);
732728

733729
// Apply the penalty to the staked PNKs.
734-
// Note that lockedPnk will always cover penalty while stakedPnk can become lower after manual unstaking.
735-
if (jurors[account].stakedPnk >= penalty) {
736-
jurors[account].stakedPnk -= penalty;
737-
} else {
738-
jurors[account].stakedPnk = 0;
739-
}
730+
sortitionModule.penalizeStake(account, penalty);
740731
emit TokenAndETHShift(
741732
account,
742733
_params.disputeID,
@@ -795,10 +786,10 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
795786
uint256 pnkLocked = (round.pnkAtStakePerJuror * degreeOfCoherence) / ALPHA_DIVISOR;
796787

797788
// Release the rest of the PNKs of the juror for this round.
798-
jurors[account].lockedPnk -= pnkLocked;
789+
sortitionModule.unlockStake(account, pnkLocked);
799790

800791
// Give back the locked PNKs in case the juror fully unstaked earlier.
801-
if (jurors[account].stakedPnk == 0) {
792+
if (!sortitionModule.isJurorStaked(account)) {
802793
pinakion.safeTransfer(account, pnkLocked);
803794
}
804795

@@ -944,17 +935,6 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
944935
return disputes[_disputeID].rounds.length;
945936
}
946937

947-
function getJurorBalance(
948-
address _juror,
949-
uint96 _courtID
950-
) external view returns (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) {
951-
Juror storage juror = jurors[_juror];
952-
totalStaked = juror.stakedPnk;
953-
totalLocked = juror.lockedPnk;
954-
stakedInCourt = juror.stakedPnkByCourt[_courtID];
955-
nbCourts = juror.courtIDs.length;
956-
}
957-
958938
function isSupported(uint96 _courtID, uint256 _disputeKitID) external view returns (bool) {
959939
return courts[_courtID].supportedDisputeKits[_disputeKitID];
960940
}
@@ -997,12 +977,6 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
997977
return disputeKits.length;
998978
}
999979

1000-
/// @dev Gets the court identifiers where a specific `_juror` has staked.
1001-
/// @param _juror The address of the juror.
1002-
function getJurorCourtIDs(address _juror) public view returns (uint96[] memory) {
1003-
return jurors[_juror].courtIDs;
1004-
}
1005-
1006980
function convertEthToTokenAmount(IERC20 _toToken, uint256 _amountInEth) public view returns (uint256) {
1007981
return (_amountInEth * 10 ** currencyRates[_toToken].rateDecimals) / currencyRates[_toToken].rateInEth;
1008982
}
@@ -1020,89 +994,34 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
1020994
emit DisputeKitEnabled(_courtID, _disputeKitID, _enable);
1021995
}
1022996

1023-
/// @dev Sets the specified juror's stake in a court.
1024-
/// `O(n + p * log_k(j))` where
1025-
/// `n` is the number of courts the juror has staked in,
1026-
/// `p` is the depth of the court tree,
1027-
/// `k` is the minimum number of children per node of one of these courts' sortition sum tree,
1028-
/// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously.
1029-
/// @param _account The address of the juror.
1030-
/// @param _courtID The ID of the court.
1031-
/// @param _newStake The new stake.
1032-
/// @return succeeded True if the call succeeded, false otherwise.
1033-
function _setStakeForAccount(
997+
function _setStake(
1034998
address _account,
1035999
uint96 _courtID,
1036-
uint256 _newStake
1037-
) internal returns (bool succeeded) {
1038-
if (_courtID == Constants.FORKING_COURT || _courtID > courts.length) return false;
1039-
1040-
Juror storage juror = jurors[_account];
1041-
uint256 currentStake = juror.stakedPnkByCourt[_courtID];
1042-
1043-
if (_newStake != 0) {
1044-
if (_newStake < courts[_courtID].minStake) return false;
1045-
} else if (currentStake == 0) {
1046-
return false;
1000+
uint256 _newStake,
1001+
bool _alreadyTransferred
1002+
) internal returns (bool success) {
1003+
if (_courtID == Constants.FORKING_COURT || _courtID > courts.length) {
1004+
return false; // Staking directly into the forking court is not allowed.
10471005
}
1048-
1049-
ISortitionModule.preStakeHookResult result = sortitionModule.preStakeHook(_account, _courtID, _newStake);
1050-
if (result == ISortitionModule.preStakeHookResult.failed) {
1051-
return false;
1052-
} else if (result == ISortitionModule.preStakeHookResult.delayed) {
1053-
emit StakeDelayed(_account, _courtID, _newStake);
1054-
return true;
1006+
if (_newStake != 0 && _newStake < courts[_courtID].minStake) {
1007+
return false; // Staking less than the minimum stake is not allowed.
10551008
}
1056-
1057-
uint256 transferredAmount;
1058-
if (_newStake >= currentStake) {
1059-
// Stake increase
1060-
// 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;
1069-
}
1070-
}
1071-
if (currentStake == 0) {
1072-
juror.courtIDs.push(_courtID);
1073-
}
1074-
} else {
1075-
// Stake decrease: make sure locked tokens always stay in the contract. They can only be released during Execution.
1076-
if (juror.stakedPnk >= currentStake - _newStake + juror.lockedPnk) {
1077-
// We have enough pnk staked to afford withdrawal while keeping locked tokens.
1078-
transferredAmount = currentStake - _newStake;
1079-
} else if (juror.stakedPnk >= juror.lockedPnk) {
1080-
// Can't afford withdrawing the current stake fully. Take whatever is available while keeping locked tokens.
1081-
transferredAmount = juror.stakedPnk - juror.lockedPnk;
1082-
}
1083-
if (transferredAmount > 0) {
1084-
if (!pinakion.safeTransfer(_account, transferredAmount)) {
1085-
return false;
1086-
}
1087-
}
1088-
if (_newStake == 0) {
1089-
for (uint256 i = juror.courtIDs.length; i > 0; i--) {
1090-
if (juror.courtIDs[i - 1] == _courtID) {
1091-
juror.courtIDs[i - 1] = juror.courtIDs[juror.courtIDs.length - 1];
1092-
juror.courtIDs.pop();
1093-
break;
1094-
}
1095-
}
1009+
(uint256 pnkDeposit, uint256 pnkWithdrawal, bool sortitionSuccess) = sortitionModule.setStake(
1010+
_account,
1011+
_courtID,
1012+
_newStake,
1013+
_alreadyTransferred
1014+
);
1015+
if (pnkDeposit > 0 && pnkWithdrawal > 0) revert StakingFailed();
1016+
if (pnkDeposit > 0) {
1017+
// 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.
1018+
pinakion.safeTransferFrom(_account, address(this), pnkDeposit);
1019+
} else if (pnkWithdrawal > 0) {
1020+
if (!pinakion.safeTransfer(_account, pnkWithdrawal)) {
1021+
return false;
10961022
}
10971023
}
1098-
1099-
// 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;
1102-
1103-
sortitionModule.setStake(_account, _courtID, _newStake);
1104-
emit StakeSet(_account, _courtID, _newStake);
1105-
return true;
1024+
return sortitionSuccess;
11061025
}
11071026

11081027
/// @dev Gets a court ID, the minimum number of jurors and an ID of a dispute kit from a specified extra data bytes array.
@@ -1143,6 +1062,8 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
11431062
// ************************************* //
11441063

11451064
error GovernorOnly();
1065+
error DisputeKitOnly();
1066+
error SortitionModuleOnly();
11461067
error UnsuccessfulCall();
11471068
error InvalidDisputKitParent();
11481069
error DepthLevelMax();
@@ -1153,7 +1074,6 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
11531074
error CannotDisableClassicDK();
11541075
error ArraysLengthMismatch();
11551076
error StakingFailed();
1156-
error WrongCaller();
11571077
error ArbitrationFeesNotEnough();
11581078
error DisputeKitNotSupportedByCourt();
11591079
error MustSupportDisputeKitClassic();
@@ -1166,7 +1086,6 @@ contract KlerosCore is IArbitratorV2, UUPSProxiable, Initializable {
11661086
error NotEvidencePeriod();
11671087
error AppealFeesNotEnough();
11681088
error DisputeNotAppealable();
1169-
error DisputeKitOnly();
11701089
error NotExecutionPeriod();
11711090
error RulingAlreadyExecuted();
11721091
error DisputePeriodIsFinal();

0 commit comments

Comments
 (0)