From a3b84707c619f52b18c732540121623d3d0a6969 Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Tue, 20 May 2025 23:36:38 +1000 Subject: [PATCH 01/12] fix(SortitionModule): fix staking logic and remove instant staking --- contracts/src/arbitration/KlerosCoreBase.sol | 35 +-- .../src/arbitration/SortitionModuleBase.sol | 222 ++++++--------- .../src/arbitration/SortitionModuleNeo.sol | 6 +- .../dispute-kits/DisputeKitClassicBase.sol | 14 +- .../interfaces/ISortitionModule.sol | 7 +- .../university/SortitionModuleUniversity.sol | 7 +- contracts/test/foundry/KlerosCore.t.sol | 254 ++++++++++-------- 7 files changed, 259 insertions(+), 286 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 6a435489b..34154ea38 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -470,15 +470,23 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _account The account whose stake is being set. /// @param _courtID The ID of the court. /// @param _newStake The new stake. - /// @param _alreadyTransferred Whether the PNKs have already been transferred to the contract. function setStakeBySortitionModule( address _account, uint96 _courtID, uint256 _newStake, - bool _alreadyTransferred + bool /*_alreadyTransferred*/ ) external { if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); - _setStake(_account, _courtID, _newStake, _alreadyTransferred, OnError.Return); + _setStake(_account, _courtID, _newStake, false, OnError.Return); // alreadyTransferred is unused and DEPRECATED. + } + + /// @dev Transfers PNK to the juror by SortitionModule. + /// @param _account The account of the juror whose PNK to transfer. + /// @param _amount The amount to transfer. + function transferBySortitionModule(address _account, uint256 _amount) external { + if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); + // Note eligibility is checked in SortitionModule. + pinakion.safeTransfer(_account, _amount); } /// @inheritdoc IArbitratorV2 @@ -774,26 +782,25 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // Fully coherent jurors won't be penalized. uint256 penalty = (round.pnkAtStakePerJuror * (ALPHA_DIVISOR - degreeOfCoherence)) / ALPHA_DIVISOR; - _params.pnkPenaltiesInRound += penalty; // Unlock the PNKs affected by the penalty address account = round.drawnJurors[_params.repartition]; sortitionModule.unlockStake(account, penalty); // Apply the penalty to the staked PNKs. - sortitionModule.penalizeStake(account, penalty); + (uint256 pnkBalance, uint256 availablePenalty) = sortitionModule.penalizeStake(account, penalty); + _params.pnkPenaltiesInRound += availablePenalty; emit TokenAndETHShift( account, _params.disputeID, _params.round, degreeOfCoherence, - -int256(penalty), + -int256(availablePenalty), 0, round.feeToken ); - - if (!disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { - // The juror is inactive, unstake them. + // Unstake the juror from all courts if he was inactive or his balance can't cover penalties anymore. + if (pnkBalance == 0 || !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { sortitionModule.setJurorInactive(account); } if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { @@ -844,11 +851,6 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // Release the rest of the PNKs of the juror for this round. sortitionModule.unlockStake(account, pnkLocked); - // Give back the locked PNKs in case the juror fully unstaked earlier. - if (!sortitionModule.isJurorStaked(account)) { - pinakion.safeTransfer(account, pnkLocked); - } - // Transfer the rewards uint256 pnkReward = ((_params.pnkPenaltiesInRound / _params.coherentCount) * degreeOfCoherence) / ALPHA_DIVISOR; round.sumPnkRewardPaid += pnkReward; @@ -1074,14 +1076,13 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _account The account to set the stake for. /// @param _courtID The ID of the court to set the stake for. /// @param _newStake The new stake. - /// @param _alreadyTransferred Whether the PNKs were already transferred to/from the staking contract. /// @param _onError Whether to revert or return false on error. /// @return Whether the stake was successfully set or not. function _setStake( address _account, uint96 _courtID, uint256 _newStake, - bool _alreadyTransferred, + bool /*_alreadyTransferred*/, OnError _onError ) internal returns (bool) { if (_courtID == FORKING_COURT || _courtID >= courts.length) { @@ -1096,7 +1097,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable _account, _courtID, _newStake, - _alreadyTransferred + false // Unused parameter. ); if (stakingResult != StakingResult.Successful) { _stakingFailed(_onError, stakingResult); diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index edb10edf1..b4594cdac 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -25,13 +25,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // * Enums / Structs * // // ************************************* // - enum PreStakeHookResult { - ok, // Correct phase. All checks are passed. - stakeDelayedAlreadyTransferred, // Wrong phase but stake is increased, so transfer the tokens without updating the drawing chance. - stakeDelayedNotTransferred, // Wrong phase and stake is decreased. Delay the token transfer and drawing chance update. - failed // Checks didn't pass. Do no changes. - } - struct SortitionSumTree { uint256 K; // The maximum number of children per node. // We use this to keep track of vacant positions in the tree after removing a leaf. This is for keeping the tree as balanced as possible without spending gas on moving nodes around. @@ -46,13 +39,13 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr address account; // The address of the juror. uint96 courtID; // The ID of the court. uint256 stake; // The new stake. - bool alreadyTransferred; // True if tokens were already transferred before delayed stake's execution. + bool alreadyTransferred; // DEPRECATED. True if tokens were already transferred before delayed stake's execution. } struct Juror { 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`. uint256 stakedPnk; // The juror's total amount of tokens staked in subcourts. Reflects actual pnk balance. - uint256 lockedPnk; // The juror's total amount of tokens locked in disputes. Can reflect actual pnk balance when stakedPnk are fully withdrawn. + uint256 lockedPnk; // The juror's total amount of tokens locked in disputes. } // ************************************* // @@ -75,7 +68,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr mapping(bytes32 treeHash => SortitionSumTree) sortitionSumTrees; // The mapping trees by keys. mapping(address account => Juror) public jurors; // The jurors. mapping(uint256 => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking. - mapping(address jurorAccount => mapping(uint96 courtId => 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]. + mapping(address jurorAccount => mapping(uint96 courtId => uint256)) public latestDelayedStakeIndex; // DEPRECATED. 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]. // ************************************* // // * Events * // @@ -88,30 +81,41 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// @param _amountAllCourts The amount of tokens staked in all courts. event StakeSet(address indexed _address, uint256 _courtID, uint256 _amount, uint256 _amountAllCourts); - /// @notice Emitted when a juror's stake is delayed and tokens are not transferred yet. + /// @notice DEPRECATED Emitted when a juror's stake is delayed and tokens are not transferred yet. /// @param _address The address of the juror. /// @param _courtID The ID of the court. /// @param _amount The amount of tokens staked in the court. event StakeDelayedNotTransferred(address indexed _address, uint256 _courtID, uint256 _amount); - /// @notice Emitted when a juror's stake is delayed and tokens are already deposited. + /// @notice DEPRECATED Emitted when a juror's stake is delayed and tokens are already deposited. /// @param _address The address of the juror. /// @param _courtID The ID of the court. /// @param _amount The amount of tokens staked in the court. event StakeDelayedAlreadyTransferredDeposited(address indexed _address, uint256 _courtID, uint256 _amount); - /// @notice Emitted when a juror's stake is delayed and tokens are already withdrawn. + /// @notice DEPRECATED Emitted when a juror's stake is delayed and tokens are already withdrawn. /// @param _address The address of the juror. /// @param _courtID The ID of the court. /// @param _amount The amount of tokens withdrawn. event StakeDelayedAlreadyTransferredWithdrawn(address indexed _address, uint96 indexed _courtID, uint256 _amount); + /// @notice Emitted when a juror's stake is delayed. + /// @param _address The address of the juror. + /// @param _courtID The ID of the court. + /// @param _amount The amount of tokens staked in the court. + event StakeDelayed(address indexed _address, uint96 indexed _courtID, uint256 _amount); + /// @notice Emitted when a juror's stake is locked. /// @param _address The address of the juror. /// @param _relativeAmount The amount of tokens locked. /// @param _unlock Whether the stake is locked or unlocked. event StakeLocked(address indexed _address, uint256 _relativeAmount, bool _unlock); + /// @dev Emitted when leftover PNK is withdrawn. + /// @param _account The account of the juror withdrawing PNK. + /// @param _amount The amount of PNK withdrawn. + event LeftoverPNKWithdrawn(address indexed _account, uint256 _amount); + // ************************************* // // * Constructor * // // ************************************* // @@ -237,18 +241,13 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) { DelayedStake storage delayedStake = delayedStakes[i]; - // Delayed stake could've been manually removed already. In this case simply move on to the next item. - if (delayedStake.account != address(0)) { - // Nullify the index so the delayed stake won't get deleted before its own execution. - delete latestDelayedStakeIndex[delayedStake.account][delayedStake.courtID]; - core.setStakeBySortitionModule( - delayedStake.account, - delayedStake.courtID, - delayedStake.stake, - delayedStake.alreadyTransferred - ); - delete delayedStakes[i]; - } + core.setStakeBySortitionModule( + delayedStake.account, + delayedStake.courtID, + delayedStake.stake, + false // Unused parameter. + ); + delete delayedStakes[i]; } delayedStakeReadIndex = newDelayedStakeReadIndex; } @@ -274,7 +273,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// @param _account The address of the juror. /// @param _courtID The ID of the court. /// @param _newStake The new stake. - /// @param _alreadyTransferred True if the tokens were already transferred from juror. Only relevant for delayed stakes. /// @return pnkDeposit The amount of PNK to be deposited. /// @return pnkWithdrawal The amount of PNK to be withdrawn. /// @return stakingResult The result of the staking operation. @@ -282,18 +280,18 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr address _account, uint96 _courtID, uint256 _newStake, - bool _alreadyTransferred + bool /*_alreadyTransferred*/ ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - (pnkDeposit, pnkWithdrawal, stakingResult) = _setStake(_account, _courtID, _newStake, _alreadyTransferred); + (pnkDeposit, pnkWithdrawal, stakingResult) = _setStake(_account, _courtID, _newStake, false); // The last parameter is unused. } /// @dev Sets the specified juror's stake in a court. - /// Note: no state changes should be made when returning `succeeded` = false, otherwise delayed stakes might break invariants. + /// Note: no state changes should be made when returning stakingResult != Successful, otherwise delayed stakes might break invariants. function _setStake( address _account, uint96 _courtID, uint256 _newStake, - bool _alreadyTransferred + bool /*_alreadyTransferred*/ ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { Juror storage juror = jurors[_account]; uint256 currentStake = stakeOf(_account, _courtID); @@ -307,35 +305,43 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr return (0, 0, StakingResult.CannotStakeZeroWhenNoStake); // Forbid staking 0 amount when current stake is 0 to avoid flaky behaviour. } - pnkWithdrawal = _deleteDelayedStake(_courtID, _account); if (phase != Phase.staking) { // Store the stake change as delayed, to be applied when the phase switches back to Staking. DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; delayedStake.account = _account; delayedStake.courtID = _courtID; delayedStake.stake = _newStake; - latestDelayedStakeIndex[_account][_courtID] = delayedStakeWriteIndex; - if (_newStake > currentStake) { - // PNK deposit: tokens are transferred now. - delayedStake.alreadyTransferred = true; - pnkDeposit = _increaseStake(juror, _courtID, _newStake, currentStake); - emit StakeDelayedAlreadyTransferredDeposited(_account, _courtID, _newStake); - } else { - // PNK withdrawal: tokens are not transferred yet. - emit StakeDelayedNotTransferred(_account, _courtID, _newStake); - } + emit StakeDelayed(_account, _courtID, _newStake); return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); } - // Current phase is Staking: set normal stakes or delayed stakes (which may have been already transferred). + // Current phase is Staking: set normal stakes or delayed stakes. if (_newStake >= currentStake) { - if (!_alreadyTransferred) { - pnkDeposit = _increaseStake(juror, _courtID, _newStake, currentStake); + pnkDeposit = _newStake - currentStake; + if (currentStake == 0) { + juror.courtIDs.push(_courtID); } + // Increase juror's balance by deposited amount. + juror.stakedPnk += pnkDeposit; } else { - pnkWithdrawal += _decreaseStake(juror, _courtID, _newStake, currentStake); + pnkWithdrawal = currentStake - _newStake; + // Ensure locked tokens remain in the contract. They can only be released during Execution. + uint256 possibleWithdrawal = juror.stakedPnk > juror.lockedPnk ? juror.stakedPnk - juror.lockedPnk : 0; + if (pnkWithdrawal > possibleWithdrawal) { + pnkWithdrawal = possibleWithdrawal; + } + juror.stakedPnk -= pnkWithdrawal; + if (_newStake == 0) { + // Cleanup + for (uint256 i = juror.courtIDs.length; i > 0; i--) { + if (juror.courtIDs[i - 1] == _courtID) { + juror.courtIDs[i - 1] = juror.courtIDs[juror.courtIDs.length - 1]; + juror.courtIDs.pop(); + break; + } + } + } } - // Update the sortition sum tree. bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID); bool finished = false; @@ -353,94 +359,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); } - /// @dev Checks if there is already a delayed stake. In this case consider it irrelevant and remove it. - /// @param _courtID ID of the court. - /// @param _juror Juror whose stake to check. - function _deleteDelayedStake(uint96 _courtID, address _juror) internal returns (uint256 actualAmountToWithdraw) { - uint256 latestIndex = latestDelayedStakeIndex[_juror][_courtID]; - if (latestIndex != 0) { - DelayedStake storage delayedStake = delayedStakes[latestIndex]; - if (delayedStake.alreadyTransferred) { - // Sortition stake represents the stake value that was last updated during Staking phase. - uint256 sortitionStake = stakeOf(_juror, _courtID); - - // Withdraw the tokens that were added with the latest delayed stake. - uint256 amountToWithdraw = delayedStake.stake - sortitionStake; - actualAmountToWithdraw = amountToWithdraw; - Juror storage juror = jurors[_juror]; - if (juror.stakedPnk <= actualAmountToWithdraw) { - actualAmountToWithdraw = juror.stakedPnk; - } - - // StakePnk can become lower because of penalty. - juror.stakedPnk -= actualAmountToWithdraw; - emit StakeDelayedAlreadyTransferredWithdrawn(_juror, _courtID, amountToWithdraw); - - if (sortitionStake == 0) { - // Cleanup: delete the court otherwise it will be duplicated after staking. - for (uint256 i = juror.courtIDs.length; i > 0; i--) { - if (juror.courtIDs[i - 1] == _courtID) { - juror.courtIDs[i - 1] = juror.courtIDs[juror.courtIDs.length - 1]; - juror.courtIDs.pop(); - break; - } - } - } - } - delete delayedStakes[latestIndex]; - delete latestDelayedStakeIndex[_juror][_courtID]; - } - } - - function _increaseStake( - Juror storage juror, - uint96 _courtID, - uint256 _newStake, - uint256 _currentStake - ) internal returns (uint256 transferredAmount) { - // Stake increase - // When stakedPnk becomes lower than lockedPnk count the locked tokens in when transferring tokens from juror. - // (E.g. stakedPnk = 0, lockedPnk = 150) which can happen if the juror unstaked fully while having some tokens locked. - uint256 previouslyLocked = (juror.lockedPnk >= juror.stakedPnk) ? juror.lockedPnk - juror.stakedPnk : 0; // underflow guard - transferredAmount = (_newStake >= _currentStake + previouslyLocked) // underflow guard - ? _newStake - _currentStake - previouslyLocked - : 0; - if (_currentStake == 0) { - juror.courtIDs.push(_courtID); - } - // stakedPnk can become async with _currentStake (e.g. after penalty). - juror.stakedPnk = (juror.stakedPnk >= _currentStake) ? juror.stakedPnk - _currentStake + _newStake : _newStake; - } - - function _decreaseStake( - Juror storage juror, - uint96 _courtID, - uint256 _newStake, - uint256 _currentStake - ) internal returns (uint256 transferredAmount) { - // Stakes can be partially delayed only when stake is increased. - // Stake decrease: make sure locked tokens always stay in the contract. They can only be released during Execution. - if (juror.stakedPnk >= _currentStake - _newStake + juror.lockedPnk) { - // We have enough pnk staked to afford withdrawal while keeping locked tokens. - transferredAmount = _currentStake - _newStake; - } else if (juror.stakedPnk >= juror.lockedPnk) { - // Can't afford withdrawing the current stake fully. Take whatever is available while keeping locked tokens. - transferredAmount = juror.stakedPnk - juror.lockedPnk; - } - if (_newStake == 0) { - // Cleanup - for (uint256 i = juror.courtIDs.length; i > 0; i--) { - if (juror.courtIDs[i - 1] == _courtID) { - juror.courtIDs[i - 1] = juror.courtIDs[juror.courtIDs.length - 1]; - juror.courtIDs.pop(); - break; - } - } - } - // stakedPnk can become async with _currentStake (e.g. after penalty). - juror.stakedPnk = (juror.stakedPnk >= _currentStake) ? juror.stakedPnk - _currentStake + _newStake : _newStake; - } - function lockStake(address _account, uint256 _relativeAmount) external override onlyByCore { jurors[_account].lockedPnk += _relativeAmount; emit StakeLocked(_account, _relativeAmount, false); @@ -451,13 +369,23 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr emit StakeLocked(_account, _relativeAmount, true); } - function penalizeStake(address _account, uint256 _relativeAmount) external override onlyByCore { + function penalizeStake( + address _account, + uint256 _relativeAmount + ) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) { Juror storage juror = jurors[_account]; - if (juror.stakedPnk >= _relativeAmount) { + uint256 stakedPnk = juror.stakedPnk; + + if (stakedPnk >= _relativeAmount) { + availablePenalty = _relativeAmount; juror.stakedPnk -= _relativeAmount; } else { - juror.stakedPnk = 0; // stakedPnk might become lower after manual unstaking, but lockedPnk will always cover the difference. + availablePenalty = stakedPnk; + juror.stakedPnk = 0; } + + pnkBalance = juror.stakedPnk; + return (pnkBalance, availablePenalty); } /// @dev Unstakes the inactive juror from all courts. @@ -474,6 +402,26 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } } + /// @dev Gives back the locked PNKs in case the juror fully unstaked earlier. + /// Note that since locked and staked PNK are async it is possible for the juror to have positive staked PNK balance + /// while having 0 stake in courts and 0 locked tokens (eg. when the juror fully unstaked during dispute and later got his tokens unlocked). + /// In this case the juror can use this function to withdraw the leftover tokens. + /// Also note that if the juror has some leftover PNK while not fully unstaked he'll have to manually unstake from all courts to trigger this function. + /// @param _account The juror whose PNK to withdraw. + function withdrawLeftoverPNK(address _account) external override { + Juror storage juror = jurors[_account]; + // Can withdraw the leftover PNK if fully unstaked, has no tokens locked and has positive balance. + // This withdrawal can't be triggered by calling setStake() in KlerosCore because current stake is technically 0, thus it is done via separate function. + if (juror.stakedPnk > 0 && juror.courtIDs.length == 0 && juror.lockedPnk == 0) { + uint256 amount = juror.stakedPnk; + juror.stakedPnk = 0; + core.transferBySortitionModule(_account, amount); + emit LeftoverPNKWithdrawn(_account, amount); + } else { + revert("Not eligible for withdrawal."); + } + } + // ************************************* // // * Public Views * // // ************************************* // diff --git a/contracts/src/arbitration/SortitionModuleNeo.sol b/contracts/src/arbitration/SortitionModuleNeo.sol index 2e60307d2..712150037 100644 --- a/contracts/src/arbitration/SortitionModuleNeo.sol +++ b/contracts/src/arbitration/SortitionModuleNeo.sol @@ -88,13 +88,13 @@ contract SortitionModuleNeo is SortitionModuleBase { address _account, uint96 _courtID, uint256 _newStake, - bool _alreadyTransferred + bool /*_alreadyTransferred*/ ) internal override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { uint256 currentStake = stakeOf(_account, _courtID); bool stakeIncrease = _newStake > currentStake; uint256 stakeChange = stakeIncrease ? _newStake - currentStake : currentStake - _newStake; Juror storage juror = jurors[_account]; - if (stakeIncrease && !_alreadyTransferred) { + if (stakeIncrease) { if (juror.stakedPnk + stakeChange > maxStakePerJuror) { return (0, 0, StakingResult.CannotStakeMoreThanMaxStakePerJuror); } @@ -113,7 +113,7 @@ contract SortitionModuleNeo is SortitionModuleBase { _account, _courtID, _newStake, - _alreadyTransferred + false // This parameter is not used ); } } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 485fe3887..1261d4f0a 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -617,7 +617,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// Note that we don't check the minStake requirement here because of the implicit staking in parent courts. /// minStake is checked directly during staking process however it's possible for the juror to get drawn /// while having < minStake if it is later increased by governance. - /// This issue is expected and harmless since we check for insolvency anyway. + /// This issue is expected and harmless. /// @param _round The round in which the juror is being drawn. /// @param _coreDisputeID ID of the dispute in the core contract. /// @param _juror Chosen address. @@ -627,19 +627,13 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi uint256 _coreDisputeID, address _juror ) internal view virtual returns (bool result) { - (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); - uint256 lockedAmountPerJuror = core.getPnkAtStakePerJuror( - _coreDisputeID, - core.getNumberOfRounds(_coreDisputeID) - 1 - ); - (uint256 totalStaked, uint256 totalLocked, , ) = core.sortitionModule().getJurorBalance(_juror, courtID); - result = totalStaked >= totalLocked + lockedAmountPerJuror; - if (singleDrawPerJuror) { uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; Dispute storage dispute = disputes[localDisputeID]; uint256 localRoundID = dispute.rounds.length - 1; - result = result && !alreadyDrawn[localDisputeID][localRoundID][_juror]; + result = !alreadyDrawn[localDisputeID][localRoundID][_juror]; + } else { + result = true; } } } diff --git a/contracts/src/arbitration/interfaces/ISortitionModule.sol b/contracts/src/arbitration/interfaces/ISortitionModule.sol index c68490222..5376d371e 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModule.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModule.sol @@ -27,7 +27,10 @@ interface ISortitionModule { function unlockStake(address _account, uint256 _relativeAmount) external; - function penalizeStake(address _account, uint256 _relativeAmount) external; + function penalizeStake( + address _account, + uint256 _relativeAmount + ) external returns (uint256 pnkBalance, uint256 availablePenalty); function notifyRandomNumber(uint256 _drawnNumber) external; @@ -45,4 +48,6 @@ interface ISortitionModule { function createDisputeHook(uint256 _disputeID, uint256 _roundID) external; function postDrawHook(uint256 _disputeID, uint256 _roundID) external; + + function withdrawLeftoverPNK(address _account) external; } diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index b178c8b75..2a70f54bf 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -235,7 +235,10 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, emit StakeLocked(_account, _relativeAmount, true); } - function penalizeStake(address _account, uint256 _relativeAmount) external override onlyByCore { + function penalizeStake( + address _account, + uint256 _relativeAmount + ) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) { Juror storage juror = jurors[_account]; if (juror.stakedPnk >= _relativeAmount) { juror.stakedPnk -= _relativeAmount; @@ -258,6 +261,8 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, } } + function withdrawLeftoverPNK(address _account) external override {} + // ************************************* // // * Public Views * // // ************************************* // diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index 58ea8ce60..dccf2fc3a 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -1010,7 +1010,7 @@ contract KlerosCoreTest is Test { vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayedAlreadyTransferredDeposited(staker1, GENERAL_COURT, 1500); + emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1500); core.setStake(GENERAL_COURT, 1500); uint256 delayedStakeId = sortitionModule.delayedStakeWriteIndex(); @@ -1022,11 +1022,11 @@ contract KlerosCoreTest is Test { assertEq(account, staker1, "Wrong staker account"); assertEq(courtID, GENERAL_COURT, "Wrong court id"); assertEq(stake, 1500, "Wrong amount staked in court"); - assertEq(alreadyTransferred, true, "Should be flagged as transferred"); + assertEq(alreadyTransferred, false, "Should be flagged as transferred"); (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule .getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 1500, "Wrong amount total staked"); + assertEq(totalStaked, 1000, "Wrong amount total staked"); assertEq(totalLocked, 0, "Wrong amount locked"); assertEq(stakedInCourt, 1000, "Amount staked in court should not change until delayed stake is executed"); assertEq(nbCourts, 1, "Wrong number of courts"); @@ -1036,9 +1036,8 @@ contract KlerosCoreTest is Test { assertEq(courts[0], GENERAL_COURT, "Wrong court id"); assertEq(sortitionModule.isJurorStaked(staker1), true, "Juror should be staked"); - assertEq(pinakion.balanceOf(address(core)), 1500, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of staker1"); - assertEq(pinakion.allowance(staker1, address(core)), 999999999999998500, "Wrong allowance amount"); + assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); } function test_setStake_decreaseDrawingPhase() public { @@ -1057,7 +1056,7 @@ contract KlerosCoreTest is Test { vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayedNotTransferred(staker1, GENERAL_COURT, 1800); + emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1800); core.setStake(GENERAL_COURT, 1800); (uint256 totalStaked, , uint256 stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); @@ -1092,12 +1091,12 @@ contract KlerosCoreTest is Test { assertEq(pinakion.balanceOf(address(core)), 10000, "Wrong token balance of the core"); assertEq(pinakion.balanceOf(staker1), 999999999999990000, "Wrong token balance of staker1"); - // Unstake to check that locked tokens will remain + // Unstake to check that locked tokens won't be withdrawn vm.prank(staker1); core.setStake(GENERAL_COURT, 0); (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 0, "Wrong amount total staked"); + assertEq(totalStaked, 3000, "Wrong amount total staked"); assertEq(totalLocked, 3000, "Wrong amount locked"); assertEq(stakedInCourt, 0, "Wrong amount staked in court"); assertEq(nbCourts, 0, "Wrong amount staked in court"); @@ -1105,19 +1104,18 @@ contract KlerosCoreTest is Test { assertEq(pinakion.balanceOf(address(core)), 3000, "Wrong token balance of the core"); assertEq(pinakion.balanceOf(staker1), 999999999999997000, "Wrong token balance of staker1"); - // Stake again to see that locked tokens will count when increasing the stake. We check that the court won't take the full stake - // but only the remaining part. + // Stake again to check the behaviour. vm.prank(staker1); core.setStake(GENERAL_COURT, 5000); (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 5000, "Wrong amount total staked"); + assertEq(totalStaked, 8000, "Wrong amount total staked"); // 5000 were added to the previous 3000. assertEq(totalLocked, 3000, "Wrong amount locked"); assertEq(stakedInCourt, 5000, "Wrong amount staked in court"); assertEq(nbCourts, 1, "Wrong amount staked in court"); - assertEq(pinakion.balanceOf(address(core)), 5000, "Locked tokens should stay in the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999995000, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(address(core)), 8000, "Wrong amount of tokens in Core"); + assertEq(pinakion.balanceOf(staker1), 999999999999992000, "Wrong token balance of staker1"); } function test_executeDelayedStakes() public { @@ -1141,18 +1139,26 @@ contract KlerosCoreTest is Test { vm.expectRevert(bytes("Should be in Staking phase.")); sortitionModule.executeDelayedStakes(5); + // Create delayed stake vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1500); core.setStake(GENERAL_COURT, 1500); - assertEq(pinakion.balanceOf(address(core)), 11500, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(address(core)), 10000, "Wrong token balance of the core"); // Balance should not increase because the stake was delayed + assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); + + // Create delayed stake for another staker vm.prank(staker2); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeDelayed(staker2, GENERAL_COURT, 0); core.setStake(GENERAL_COURT, 0); assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); // Balance should not change since wrong phase + // Create another delayed stake for staker1 on top of it to check the execution vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayedAlreadyTransferredWithdrawn(staker1, GENERAL_COURT, 1500); + emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1800); core.setStake(GENERAL_COURT, 1800); assertEq(sortitionModule.delayedStakeWriteIndex(), 3, "Wrong delayedStakeWriteIndex"); @@ -1160,42 +1166,47 @@ contract KlerosCoreTest is Test { (address account, uint96 courtID, uint256 stake, bool alreadyTransferred) = sortitionModule.delayedStakes(1); - // First delayed stake should be nullified - assertEq(account, address(0), "Wrong staker account after delayed stake deletion"); - assertEq(courtID, 0, "Court id should be nullified"); - assertEq(stake, 0, "No amount to stake"); + // Check each delayed stake + assertEq(account, staker1, "Wrong staker account for the first delayed stake"); + assertEq(courtID, GENERAL_COURT, "Wrong court ID"); + assertEq(stake, 1500, "Wrong staking amount"); assertEq(alreadyTransferred, false, "Should be false"); (account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(2); assertEq(account, staker2, "Wrong staker2 account"); assertEq(courtID, GENERAL_COURT, "Wrong court id for staker2"); assertEq(stake, 0, "Wrong amount for delayed stake of staker2"); - assertEq(alreadyTransferred, false, "Should be false for staker2"); + assertEq(alreadyTransferred, false, "Should be false"); (account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(3); assertEq(account, staker1, "Wrong staker1 account"); assertEq(courtID, GENERAL_COURT, "Wrong court id for staker1"); assertEq(stake, 1800, "Wrong amount for delayed stake of staker1"); - assertEq(alreadyTransferred, true, "Should be true for staker1"); + assertEq(alreadyTransferred, false, "Should be false"); - assertEq(pinakion.balanceOf(address(core)), 11800, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999998200, "Wrong token balance of staker1"); + // So far the only amount transferred was 10000 by staker2. Staker 1 has two delayed stakes, for 1500 and 1800 pnk. + assertEq(pinakion.balanceOf(address(core)), 10000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule - .getJurorBalance(staker1, GENERAL_COURT); // Only check the first staker since he has consecutive delayed stakes - assertEq(totalStaked, 1800, "Wrong amount total staked"); + .getJurorBalance(staker1, GENERAL_COURT); // Only check the first staker to check how consecutive delayed stakes are handled. + // Balances shouldn't be updated yet. + assertEq(totalStaked, 0, "Wrong amount total staked"); assertEq(totalLocked, 0, "Wrong amount locked"); assertEq(stakedInCourt, 0, "Wrong amount staked in court"); - assertEq(nbCourts, 1, "Wrong amount staked in court"); + assertEq(nbCourts, 0, "Wrong number of courts"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Staking. Delayed stakes can be executed now vm.prank(address(core)); pinakion.transfer(governor, 10000); // Dispose of the tokens of 2nd staker to make the execution fail for the 2nd delayed stake - assertEq(pinakion.balanceOf(address(core)), 1800, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); + // 2 events should be emitted but the 2nd stake supersedes the first one in the end. + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1500, 1500); vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1800, 1800); sortitionModule.executeDelayedStakes(20); // Deliberately ask for more iterations than needed @@ -1225,54 +1236,6 @@ contract KlerosCoreTest is Test { assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); } - function test_deleteDelayedStake() public { - // Check that the delayed stake gets deleted without execution if the juror changed his stake in staking phase before its execution. - vm.prank(staker1); - core.setStake(GENERAL_COURT, 1000); - - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); - sortitionModule.passPhase(); // Drawing phase - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 1500); // Create delayed stake - - (uint256 totalStaked, , uint256 stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 1500, "Wrong amount total staked"); - assertEq(stakedInCourt, 1000, "Wrong amount staked in court"); - assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of the staker1"); - assertEq(pinakion.balanceOf(address(core)), 1500, "Wrong token balance of the core"); - - (address account, uint96 courtID, uint256 stake, bool alreadyTransferred) = sortitionModule.delayedStakes(1); - assertEq(account, staker1, "Wrong account"); - assertEq(courtID, GENERAL_COURT, "Wrong court id"); - assertEq(stake, 1500, "Wrong amount for delayed stake"); - assertEq(alreadyTransferred, true, "Should be true"); - - vm.warp(block.timestamp + maxDrawingTime); - sortitionModule.passPhase(); // Staking phase - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 1700); // Set stake 2nd time, this time in staking phase to see that the delayed stake will be nullified. - - (totalStaked, , stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 1700, "Wrong amount total staked"); - assertEq(stakedInCourt, 1700, "Wrong amount staked in court"); - assertEq(pinakion.balanceOf(staker1), 999999999999998300, "Wrong token balance of the staker1"); - assertEq(pinakion.balanceOf(address(core)), 1700, "Wrong token balance of the core"); - - sortitionModule.executeDelayedStakes(1); - (account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(1); - // Check that delayed stake is deleted - assertEq(account, address(0), "Wrong staker account after delayed stake deletion"); - assertEq(courtID, 0, "Court id should be nullified"); - assertEq(stake, 0, "No amount to stake"); - assertEq(alreadyTransferred, false, "Should be false"); - } - function test_setStakeBySortitionModule() public { // Note that functionality of this function was checked during delayed stakes execution vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector); @@ -1464,43 +1427,17 @@ contract KlerosCoreTest is Test { vm.expectEmit(true, true, true, true); emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 0); // VoteID = 0 - core.draw(disputeID, DEFAULT_NB_OF_JURORS); // Do 3 iterations, but the current stake will only allow 1. + core.draw(disputeID, DEFAULT_NB_OF_JURORS); // Do 3 iterations and see that the juror will get drawn 3 times despite low stake. (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, ) = sortitionModule.getJurorBalance( staker1, GENERAL_COURT ); assertEq(totalStaked, 1500, "Wrong amount total staked"); - assertEq(totalLocked, 1000, "Wrong amount locked"); // 1000 per draw - assertEq(stakedInCourt, 1500, "Wrong amount staked in court"); - assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count"); - - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); - assertEq(round.drawIterations, 3, "Wrong drawIterations number"); - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 3000); // Set stake to the minimal amount to cover the full dispute. The stake will be updated in Drawing phase since it's an increase. - - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker1, 1000, false); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 1); // VoteID = 1 - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker1, 1000, false); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 2); // VoteID = 2 - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - - (totalStaked, totalLocked, stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 3000, "Wrong amount total staked"); - assertEq(totalLocked, 3000, "Wrong amount locked"); // 1000 per draw and the juror was drawn 3 times + assertEq(totalLocked, 3000, "Wrong amount locked"); // 1000 per draw assertEq(stakedInCourt, 1500, "Wrong amount staked in court"); assertEq(sortitionModule.disputesWithoutJurors(), 0, "Wrong disputesWithoutJurors count"); - round = core.getRoundInfo(disputeID, roundID); - assertEq(round.drawIterations, 5, "Wrong drawIterations number"); // It's 5 because we needed only 2 iterations to draw the rest of the jurors - for (uint256 i = 0; i < DEFAULT_NB_OF_JURORS; i++) { (address account, bytes32 commit, uint256 choice, bool voted) = disputeKit.getVoteInfo(0, 0, i); assertEq(account, staker1, "Wrong drawn account"); @@ -2475,6 +2412,9 @@ contract KlerosCoreTest is Test { (, , , uint256 nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); assertEq(nbCourts, 2, "Wrong number of courts"); + assertEq(pinakion.balanceOf(address(core)), 40000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999960000, "Wrong token balance of staker1"); + vm.prank(disputer); arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); @@ -2497,26 +2437,85 @@ contract KlerosCoreTest is Test { uint256 governorTokenBalance = pinakion.balanceOf(governor); + // Note that these events are emitted only after the first iteration of execute() therefore the juror has been penalized only for 1000 PNK her. vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, newCourtID, 0, 19000); + emit SortitionModuleBase.StakeSet(staker1, newCourtID, 0, 19000); // Starting with 40000 we first nullify the stake and remove 20000 and then remove penalty once since there was only first iteration (40000 - 20000 - 1000) vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 0, 0); + emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 0, 2000); // 2000 PNK should remain in balance to cover penalties since the first 1000 of locked pnk was already unlocked core.execute(disputeID, 0, 3); assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999997000, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(staker1), 999999999999997000, "Wrong token balance of staker1"); // 3000 locked PNK was withheld by the contract and given to governor. assertEq(pinakion.balanceOf(governor), governorTokenBalance + 3000, "Wrong token balance of governor"); (, , , nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); assertEq(nbCourts, 0, "Should unstake from all courts"); } - function test_execute_RewardUnstaked() public { - // Reward the juror who fully unstaked earlier. Return the locked tokens + function test_execute_UnstakeInsolvent() public { uint256 disputeID = 0; vm.prank(staker1); - core.setStake(GENERAL_COURT, 20000); + core.setStake(GENERAL_COURT, 1000); + + assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); + + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + (uint256 totalStaked, uint256 totalLocked, , uint256 nbCourts) = sortitionModule.getJurorBalance( + staker1, + GENERAL_COURT + ); + assertEq(totalStaked, 1000, "Wrong totalStaked"); + assertEq(totalLocked, 3000, "totalLocked should exceed totalStaked"); // Juror only staked 1000 but was drawn 3x of minStake (3000 locked) + assertEq(nbCourts, 1, "Wrong number of courts"); + + sortitionModule.passPhase(); // Staking phase. Change to staking so we don't have to deal with delayed stakes. + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](1); + voteIDs[0] = 0; + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZ"); // 1 incoherent vote should make the juror insolvent + + voteIDs = new uint256[](2); + voteIDs[0] = 1; + voteIDs[1] = 2; + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 0, 0); // Juror should have no stake left and should be unstaked from the court automatically. + core.execute(disputeID, 0, 6); + + assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); // The juror should have his penalty back as a reward + + (, , , nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(nbCourts, 0, "Should unstake from all courts"); + } + + function test_execute_withdrawLeftoverPNK() public { + // Return the previously locked tokens + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1000); vm.prank(disputer); arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); @@ -2544,27 +2543,48 @@ contract KlerosCoreTest is Test { core.passPeriod(disputeID); // Execution vm.prank(staker1); - core.setStake(GENERAL_COURT, 0); + core.setStake(GENERAL_COURT, 0); // Set stake to 0 to check if it will be withdrawn later. (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule .getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 0, "Should be unstaked"); + assertEq(totalStaked, 1000, "Wrong amount staked"); assertEq(totalLocked, 3000, "Wrong amount locked"); assertEq(stakedInCourt, 0, "Should be unstaked"); assertEq(nbCourts, 0, "Should be 0 courts"); - assertEq(pinakion.balanceOf(address(core)), 3000, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999997000, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); + + vm.expectRevert(bytes("Not eligible for withdrawal.")); + sortitionModule.withdrawLeftoverPNK(staker1); core.execute(disputeID, 0, 6); + (totalStaked, totalLocked, , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 1000, "Wrong amount staked"); + assertEq(totalLocked, 0, "Should be fully unlocked"); + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.pnkPenalties, 0, "Wrong pnkPenalties"); assertEq(round.sumFeeRewardPaid, 0.09 ether, "Wrong sumFeeRewardPaid"); assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); // No penalty so no rewards in pnk - assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); + // Execute() shouldn't withdraw the tokens. + assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); + + vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector); + vm.prank(governor); + core.transferBySortitionModule(staker1, 1000); + + sortitionModule.withdrawLeftoverPNK(staker1); + + (totalStaked, , , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 0, "Should be unstaked fully"); + + // Check that everything is withdrawn now + assertEq(pinakion.balanceOf(address(core)), 0, "Core balance should be empty"); + assertEq(pinakion.balanceOf(staker1), 1 ether, "All PNK should be withdrawn"); } function test_execute_feeToken() public { From 585ef6016319095ffd656c01a72ba02d97fd59d8 Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Sat, 31 May 2025 02:50:36 +1000 Subject: [PATCH 02/12] fix(KC): update the state after transfer --- contracts/src/arbitration/KlerosCoreBase.sol | 6 +++- .../src/arbitration/SortitionModuleBase.sol | 32 ++++++++++++++----- .../interfaces/ISortitionModule.sol | 8 +++++ .../university/SortitionModuleUniversity.sol | 8 +++++ contracts/src/libraries/Constants.sol | 1 + 5 files changed, 46 insertions(+), 9 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 34154ea38..97d52efee 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -1099,9 +1099,11 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable _newStake, false // Unused parameter. ); - if (stakingResult != StakingResult.Successful) { + if (stakingResult != StakingResult.Successful && stakingResult != StakingResult.Delayed) { _stakingFailed(_onError, stakingResult); return false; + } else if (stakingResult == StakingResult.Delayed) { + return true; } if (pnkDeposit > 0) { if (!pinakion.safeTransferFrom(_account, address(this), pnkDeposit)) { @@ -1115,6 +1117,8 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable return false; } } + sortitionModule.updateState(_account, _courtID, pnkDeposit, pnkWithdrawal, _newStake); + return true; } diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index b4594cdac..2a71588e0 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -312,17 +312,12 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr delayedStake.courtID = _courtID; delayedStake.stake = _newStake; emit StakeDelayed(_account, _courtID, _newStake); - return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); + return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed); } // Current phase is Staking: set normal stakes or delayed stakes. if (_newStake >= currentStake) { pnkDeposit = _newStake - currentStake; - if (currentStake == 0) { - juror.courtIDs.push(_courtID); - } - // Increase juror's balance by deposited amount. - juror.stakedPnk += pnkDeposit; } else { pnkWithdrawal = currentStake - _newStake; // Ensure locked tokens remain in the contract. They can only be released during Execution. @@ -330,7 +325,28 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr if (pnkWithdrawal > possibleWithdrawal) { pnkWithdrawal = possibleWithdrawal; } - juror.stakedPnk -= pnkWithdrawal; + } + return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); + } + + /// @dev Called by KC at the end of setStake flow. + function updateState( + address _account, + uint96 _courtID, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal, + uint256 _newStake + ) external override onlyByCore { + Juror storage juror = jurors[_account]; + if (_pnkDeposit > 0) { + uint256 currentStake = stakeOf(_account, _courtID); + if (currentStake == 0) { + juror.courtIDs.push(_courtID); + } + // Increase juror's balance by deposited amount. + juror.stakedPnk += _pnkDeposit; + } else { + juror.stakedPnk -= _pnkWithdrawal; if (_newStake == 0) { // Cleanup for (uint256 i = juror.courtIDs.length; i > 0; i--) { @@ -342,6 +358,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } } } + // Update the sortition sum tree. bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID); bool finished = false; @@ -356,7 +373,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } } emit StakeSet(_account, _courtID, _newStake, juror.stakedPnk); - return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); } function lockStake(address _account, uint256 _relativeAmount) external override onlyByCore { diff --git a/contracts/src/arbitration/interfaces/ISortitionModule.sol b/contracts/src/arbitration/interfaces/ISortitionModule.sol index 5376d371e..a7c1499fd 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModule.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModule.sol @@ -50,4 +50,12 @@ interface ISortitionModule { function postDrawHook(uint256 _disputeID, uint256 _roundID) external; function withdrawLeftoverPNK(address _account) external; + + function updateState( + address _account, + uint96 _courtID, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal, + uint256 _newStake + ) external; } diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index 2a70f54bf..795870e00 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -317,6 +317,14 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, return jurors[_juror].stakedPnk > 0; } + function updateState( + address _account, + uint96 _courtID, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal, + uint256 _newStake + ) external {} + // ************************************* // // * Internal * // // ************************************* // diff --git a/contracts/src/libraries/Constants.sol b/contracts/src/libraries/Constants.sol index f393b4792..f2101461b 100644 --- a/contracts/src/libraries/Constants.sol +++ b/contracts/src/libraries/Constants.sol @@ -27,6 +27,7 @@ enum OnError { enum StakingResult { Successful, + Delayed, StakingTransferFailed, UnstakingTransferFailed, CannotStakeInMoreCourts, From f30d32701c991749f7e256b8e7bd60747297cfa7 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 5 Jun 2025 14:40:32 +0100 Subject: [PATCH 03/12] chore: removed the handlers for the instant staking events --- .../abi-migrations/SortitionModuleNeo.json | 132 +++++------------ subgraph/core-neo/subgraph.yaml | 11 +- .../core/abi-migrations/SortitionModule.json | 139 ++++-------------- subgraph/core/src/SortitionModule.ts | 25 +--- subgraph/core/subgraph.yaml | 11 +- subgraph/temp-older-events-addition.txt | 2 +- 6 files changed, 77 insertions(+), 243 deletions(-) diff --git a/subgraph/core-neo/abi-migrations/SortitionModuleNeo.json b/subgraph/core-neo/abi-migrations/SortitionModuleNeo.json index b65294575..f7a756723 100644 --- a/subgraph/core-neo/abi-migrations/SortitionModuleNeo.json +++ b/subgraph/core-neo/abi-migrations/SortitionModuleNeo.json @@ -60,34 +60,15 @@ "name": "Initialized", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "enum ISortitionModule.Phase", - "name": "_phase", - "type": "uint8" - } - ], - "name": "NewPhase", - "type": "event" - }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", - "name": "_address", + "name": "_account", "type": "address" }, - { - "indexed": false, - "internalType": "uint256", - "name": "_courtID", - "type": "uint256" - }, { "indexed": false, "internalType": "uint256", @@ -95,32 +76,20 @@ "type": "uint256" } ], - "name": "StakeDelayedAlreadyTransferredDeposited", + "name": "LeftoverPNKWithdrawn", "type": "event" }, { "anonymous": false, "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_address", - "type": "address" - }, { "indexed": false, - "internalType": "uint256", - "name": "_courtID", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_amount", - "type": "uint256" + "internalType": "enum ISortitionModule.Phase", + "name": "_phase", + "type": "uint8" } ], - "name": "StakeDelayedAlreadyTransferred", + "name": "NewPhase", "type": "event" }, { @@ -145,32 +114,7 @@ "type": "uint256" } ], - "name": "StakeDelayedAlreadyTransferredWithdrawn", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_address", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_courtID", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - } - ], - "name": "StakeDelayedNotTransferred", + "name": "StakeDelayed", "type": "event" }, { @@ -449,11 +393,6 @@ "internalType": "uint256", "name": "stake", "type": "uint256" - }, - { - "internalType": "bool", - "name": "alreadyTransferred", - "type": "bool" } ], "stateMutability": "view", @@ -696,30 +635,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "jurorAccount", - "type": "address" - }, - { - "internalType": "uint96", - "name": "courtId", - "type": "uint96" - } - ], - "name": "latestDelayedStakeIndex", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -824,7 +739,18 @@ } ], "name": "penalizeStake", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "pnkBalance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "availablePenalty", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, @@ -956,7 +882,7 @@ }, { "internalType": "bool", - "name": "_alreadyTransferred", + "name": "", "type": "bool" } ], @@ -972,6 +898,11 @@ "name": "pnkWithdrawal", "type": "uint256" }, + { + "internalType": "uint256", + "name": "oldStake", + "type": "uint256" + }, { "internalType": "enum StakingResult", "name": "stakingResult", @@ -1090,6 +1021,19 @@ ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_account", + "type": "address" + } + ], + "name": "withdrawLeftoverPNK", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] } diff --git a/subgraph/core-neo/subgraph.yaml b/subgraph/core-neo/subgraph.yaml index 69e4b7d01..10a37fb5b 100644 --- a/subgraph/core-neo/subgraph.yaml +++ b/subgraph/core-neo/subgraph.yaml @@ -158,15 +158,8 @@ dataSources: # FIX: temporarily point to abi with event addition file: ./abi-migrations/SortitionModuleNeo.json eventHandlers: - - event: StakeDelayedAlreadyTransferredDeposited(indexed address,uint256,uint256) - handler: handleStakeDelayedAlreadyTransferredDeposited - # FIX: temporarily indexing old event name - - event: StakeDelayedAlreadyTransferred(indexed address,uint256,uint256) - handler: handleStakeDelayedAlreadyTransferred - - event: StakeDelayedAlreadyTransferredWithdrawn(indexed address,indexed uint96,uint256) - handler: handleStakeDelayedAlreadyTransferredWithdrawn - - event: StakeDelayedNotTransferred(indexed address,uint256,uint256) - handler: handleStakeDelayedNotTransferred + - event: StakeDelayed(indexed address,indexed uint96,uint256) + handler: handleStakeDelayed - event: StakeLocked(indexed address,uint256,bool) handler: handleStakeLocked - event: StakeSet(indexed address,uint256,uint256,uint256) diff --git a/subgraph/core/abi-migrations/SortitionModule.json b/subgraph/core/abi-migrations/SortitionModule.json index d9ba2ca9e..859f4ae9f 100644 --- a/subgraph/core/abi-migrations/SortitionModule.json +++ b/subgraph/core/abi-migrations/SortitionModule.json @@ -1,12 +1,9 @@ { "abi": [ { - "stateMutability": "payable", - "type": "fallback" - }, - { - "stateMutability": "payable", - "type": "receive" + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" }, { "inputs": [], @@ -63,34 +60,15 @@ "name": "Initialized", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "enum ISortitionModule.Phase", - "name": "_phase", - "type": "uint8" - } - ], - "name": "NewPhase", - "type": "event" - }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", - "name": "_address", + "name": "_account", "type": "address" }, - { - "indexed": false, - "internalType": "uint256", - "name": "_courtID", - "type": "uint256" - }, { "indexed": false, "internalType": "uint256", @@ -98,32 +76,20 @@ "type": "uint256" } ], - "name": "StakeDelayedAlreadyTransferredDeposited", + "name": "LeftoverPNKWithdrawn", "type": "event" }, { "anonymous": false, "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_address", - "type": "address" - }, { "indexed": false, - "internalType": "uint256", - "name": "_courtID", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_amount", - "type": "uint256" + "internalType": "enum ISortitionModule.Phase", + "name": "_phase", + "type": "uint8" } ], - "name": "StakeDelayedAlreadyTransferred", + "name": "NewPhase", "type": "event" }, { @@ -148,32 +114,7 @@ "type": "uint256" } ], - "name": "StakeDelayedAlreadyTransferredWithdrawn", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_address", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_courtID", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - } - ], - "name": "StakeDelayedNotTransferred", + "name": "StakeDelayed", "type": "event" }, { @@ -426,11 +367,6 @@ "internalType": "uint256", "name": "stake", "type": "uint256" - }, - { - "internalType": "bool", - "name": "alreadyTransferred", - "type": "bool" } ], "stateMutability": "view", @@ -663,30 +599,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "jurorAccount", - "type": "address" - }, - { - "internalType": "uint96", - "name": "courtId", - "type": "uint96" - } - ], - "name": "latestDelayedStakeIndex", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -765,7 +677,18 @@ } ], "name": "penalizeStake", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "pnkBalance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "availablePenalty", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, @@ -897,7 +820,7 @@ }, { "internalType": "bool", - "name": "_alreadyTransferred", + "name": "", "type": "bool" } ], @@ -913,6 +836,11 @@ "name": "pnkWithdrawal", "type": "uint256" }, + { + "internalType": "uint256", + "name": "oldStake", + "type": "uint256" + }, { "internalType": "enum StakingResult", "name": "stakingResult", @@ -1023,17 +951,14 @@ "inputs": [ { "internalType": "address", - "name": "_implementation", + "name": "_account", "type": "address" - }, - { - "internalType": "bytes", - "name": "_data", - "type": "bytes" } ], + "name": "withdrawLeftoverPNK", + "outputs": [], "stateMutability": "nonpayable", - "type": "constructor" + "type": "function" } ] } diff --git a/subgraph/core/src/SortitionModule.ts b/subgraph/core/src/SortitionModule.ts index 4d4f8c895..672a994ba 100644 --- a/subgraph/core/src/SortitionModule.ts +++ b/subgraph/core/src/SortitionModule.ts @@ -1,31 +1,10 @@ -import { - SortitionModule, - StakeDelayedAlreadyTransferred, - StakeDelayedAlreadyTransferredDeposited, - StakeDelayedAlreadyTransferredWithdrawn, - StakeDelayedNotTransferred, - StakeLocked, - StakeSet, -} from "../generated/SortitionModule/SortitionModule"; +import { SortitionModule, StakeDelayed, StakeLocked, StakeSet } from "../generated/SortitionModule/SortitionModule"; import { updateJurorDelayedStake, updateJurorStake } from "./entities/JurorTokensPerCourt"; import { ensureUser } from "./entities/User"; import { ZERO } from "./utils"; -// FIX: temporarily adding this handler for old event name "StakeDelayedAlreadyTransferred", delete when deploying new fresh-address contract. -export function handleStakeDelayedAlreadyTransferred(event: StakeDelayedAlreadyTransferred): void { - updateJurorDelayedStake(event.params._address.toHexString(), event.params._courtID.toString(), event.params._amount); -} - -export function handleStakeDelayedAlreadyTransferredDeposited(event: StakeDelayedAlreadyTransferredDeposited): void { - updateJurorDelayedStake(event.params._address.toHexString(), event.params._courtID.toString(), event.params._amount); -} - -export function handleStakeDelayedAlreadyTransferredWithdrawn(event: StakeDelayedAlreadyTransferredWithdrawn): void { - updateJurorDelayedStake(event.params._address.toHexString(), event.params._courtID.toString(), event.params._amount); -} - -export function handleStakeDelayedNotTransferred(event: StakeDelayedNotTransferred): void { +export function handleStakeDelayed(event: StakeDelayed): void { updateJurorDelayedStake(event.params._address.toHexString(), event.params._courtID.toString(), event.params._amount); } diff --git a/subgraph/core/subgraph.yaml b/subgraph/core/subgraph.yaml index d3aff9873..e8c0cd3f6 100644 --- a/subgraph/core/subgraph.yaml +++ b/subgraph/core/subgraph.yaml @@ -159,15 +159,8 @@ dataSources: # FIX: temporarily point to abi with event addition file: ./abi-migrations/SortitionModule.json eventHandlers: - - event: StakeDelayedAlreadyTransferredDeposited(indexed address,uint256,uint256) - handler: handleStakeDelayedAlreadyTransferredDeposited - # FIX: temporarily indexing old event name - - event: StakeDelayedAlreadyTransferred(indexed address,uint256,uint256) - handler: handleStakeDelayedAlreadyTransferred - - event: StakeDelayedAlreadyTransferredWithdrawn(indexed address,indexed uint96,uint256) - handler: handleStakeDelayedAlreadyTransferredWithdrawn - - event: StakeDelayedNotTransferred(indexed address,uint256,uint256) - handler: handleStakeDelayedNotTransferred + - event: StakeDelayed(indexed address,indexed uint96,uint256) + handler: handleStakeDelayed - event: StakeLocked(indexed address,uint256,bool) handler: handleStakeLocked - event: StakeSet(indexed address,uint256,uint256,uint256) diff --git a/subgraph/temp-older-events-addition.txt b/subgraph/temp-older-events-addition.txt index abf0ca62f..f049dcc70 100644 --- a/subgraph/temp-older-events-addition.txt +++ b/subgraph/temp-older-events-addition.txt @@ -92,7 +92,7 @@ -// Goes in SortitionModule.json: rename of StakeDelayedAlreadyTransferred => StakeDelayedAlreadyTransferredDeposited +// NOT NEEDED ANYMORE: Goes in SortitionModule.json: rename of StakeDelayedAlreadyTransferred => StakeDelayedAlreadyTransferredDeposited { "anonymous": false, "inputs": [ From c193e480f8f3a840eec6aa61b16301b6d91df573 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 15 Jul 2025 23:10:16 +0200 Subject: [PATCH 04/12] fix: removed the unused _alreadyTransferred parameter --- contracts/src/arbitration/KlerosCoreBase.sol | 22 ++++------------- contracts/src/arbitration/KlerosCoreNeo.sol | 2 +- .../src/arbitration/SortitionModuleBase.sol | 17 ++++--------- .../src/arbitration/SortitionModuleNeo.sol | 10 ++------ .../interfaces/ISortitionModule.sol | 3 +-- .../university/KlerosCoreUniversity.sol | 24 ++++--------------- .../university/SortitionModuleUniversity.sol | 12 ++++------ contracts/test/foundry/KlerosCore.t.sol | 2 +- 8 files changed, 24 insertions(+), 68 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 97d52efee..3b10dd662 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -463,21 +463,16 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _newStake The new stake. /// Note that the existing delayed stake will be nullified as non-relevant. function setStake(uint96 _courtID, uint256 _newStake) external virtual whenNotPaused { - _setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); + _setStake(msg.sender, _courtID, _newStake, OnError.Revert); } /// @dev Sets the stake of a specified account in a court, typically to apply a delayed stake or unstake inactive jurors. /// @param _account The account whose stake is being set. /// @param _courtID The ID of the court. /// @param _newStake The new stake. - function setStakeBySortitionModule( - address _account, - uint96 _courtID, - uint256 _newStake, - bool /*_alreadyTransferred*/ - ) external { + function setStakeBySortitionModule(address _account, uint96 _courtID, uint256 _newStake) external { if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); - _setStake(_account, _courtID, _newStake, false, OnError.Return); // alreadyTransferred is unused and DEPRECATED. + _setStake(_account, _courtID, _newStake, OnError.Return); } /// @dev Transfers PNK to the juror by SortitionModule. @@ -1078,13 +1073,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _newStake The new stake. /// @param _onError Whether to revert or return false on error. /// @return Whether the stake was successfully set or not. - function _setStake( - address _account, - uint96 _courtID, - uint256 _newStake, - bool /*_alreadyTransferred*/, - OnError _onError - ) internal returns (bool) { + function _setStake(address _account, uint96 _courtID, uint256 _newStake, OnError _onError) internal returns (bool) { if (_courtID == FORKING_COURT || _courtID >= courts.length) { _stakingFailed(_onError, StakingResult.CannotStakeInThisCourt); // Staking directly into the forking court is not allowed. return false; @@ -1096,8 +1085,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = sortitionModule.setStake( _account, _courtID, - _newStake, - false // Unused parameter. + _newStake ); if (stakingResult != StakingResult.Successful && stakingResult != StakingResult.Delayed) { _stakingFailed(_onError, stakingResult); diff --git a/contracts/src/arbitration/KlerosCoreNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol index 988e42a53..49019252b 100644 --- a/contracts/src/arbitration/KlerosCoreNeo.sol +++ b/contracts/src/arbitration/KlerosCoreNeo.sol @@ -105,7 +105,7 @@ contract KlerosCoreNeo is KlerosCoreBase { /// Note that the existing delayed stake will be nullified as non-relevant. function setStake(uint96 _courtID, uint256 _newStake) external override whenNotPaused { if (jurorNft.balanceOf(msg.sender) == 0) revert NotEligibleForStaking(); - super._setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); + super._setStake(msg.sender, _courtID, _newStake, OnError.Revert); } // ************************************* // diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index 2a71588e0..313a6d038 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -241,12 +241,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) { DelayedStake storage delayedStake = delayedStakes[i]; - core.setStakeBySortitionModule( - delayedStake.account, - delayedStake.courtID, - delayedStake.stake, - false // Unused parameter. - ); + core.setStakeBySortitionModule(delayedStake.account, delayedStake.courtID, delayedStake.stake); delete delayedStakes[i]; } delayedStakeReadIndex = newDelayedStakeReadIndex; @@ -279,10 +274,9 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr function setStake( address _account, uint96 _courtID, - uint256 _newStake, - bool /*_alreadyTransferred*/ + uint256 _newStake ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - (pnkDeposit, pnkWithdrawal, stakingResult) = _setStake(_account, _courtID, _newStake, false); // The last parameter is unused. + (pnkDeposit, pnkWithdrawal, stakingResult) = _setStake(_account, _courtID, _newStake); } /// @dev Sets the specified juror's stake in a court. @@ -290,8 +284,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr function _setStake( address _account, uint96 _courtID, - uint256 _newStake, - bool /*_alreadyTransferred*/ + uint256 _newStake ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { Juror storage juror = jurors[_account]; uint256 currentStake = stakeOf(_account, _courtID); @@ -414,7 +407,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr function setJurorInactive(address _account) external override onlyByCore { uint96[] memory courtIDs = getJurorCourtIDs(_account); for (uint256 j = courtIDs.length; j > 0; j--) { - core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0, false); + core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0); } } diff --git a/contracts/src/arbitration/SortitionModuleNeo.sol b/contracts/src/arbitration/SortitionModuleNeo.sol index 712150037..c5a8300d7 100644 --- a/contracts/src/arbitration/SortitionModuleNeo.sol +++ b/contracts/src/arbitration/SortitionModuleNeo.sol @@ -87,8 +87,7 @@ contract SortitionModuleNeo is SortitionModuleBase { function _setStake( address _account, uint96 _courtID, - uint256 _newStake, - bool /*_alreadyTransferred*/ + uint256 _newStake ) internal override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { uint256 currentStake = stakeOf(_account, _courtID); bool stakeIncrease = _newStake > currentStake; @@ -109,11 +108,6 @@ contract SortitionModuleNeo is SortitionModuleBase { totalStaked -= stakeChange; } } - (pnkDeposit, pnkWithdrawal, stakingResult) = super._setStake( - _account, - _courtID, - _newStake, - false // This parameter is not used - ); + (pnkDeposit, pnkWithdrawal, stakingResult) = super._setStake(_account, _courtID, _newStake); } } diff --git a/contracts/src/arbitration/interfaces/ISortitionModule.sol b/contracts/src/arbitration/interfaces/ISortitionModule.sol index a7c1499fd..732c350ef 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModule.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModule.sol @@ -17,8 +17,7 @@ interface ISortitionModule { function setStake( address _account, uint96 _courtID, - uint256 _newStake, - bool _alreadyTransferred + uint256 _newStake ) external returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult); function setJurorInactive(address _account) external; diff --git a/contracts/src/arbitration/university/KlerosCoreUniversity.sol b/contracts/src/arbitration/university/KlerosCoreUniversity.sol index e7182a809..67f310626 100644 --- a/contracts/src/arbitration/university/KlerosCoreUniversity.sol +++ b/contracts/src/arbitration/university/KlerosCoreUniversity.sol @@ -456,22 +456,16 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { /// @param _newStake The new stake. /// Note that the existing delayed stake will be nullified as non-relevant. function setStake(uint96 _courtID, uint256 _newStake) external { - _setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); + _setStake(msg.sender, _courtID, _newStake, OnError.Revert); } /// @dev Sets the stake of a specified account in a court, typically to apply a delayed stake or unstake inactive jurors. /// @param _account The account whose stake is being set. /// @param _courtID The ID of the court. /// @param _newStake The new stake. - /// @param _alreadyTransferred Whether the PNKs have already been transferred to the contract. - function setStakeBySortitionModule( - address _account, - uint96 _courtID, - uint256 _newStake, - bool _alreadyTransferred - ) external { + function setStakeBySortitionModule(address _account, uint96 _courtID, uint256 _newStake) external { if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); - _setStake(_account, _courtID, _newStake, _alreadyTransferred, OnError.Return); + _setStake(_account, _courtID, _newStake, OnError.Return); } /// @inheritdoc IArbitratorV2 @@ -1042,16 +1036,9 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { /// @param _account The account to set the stake for. /// @param _courtID The ID of the court to set the stake for. /// @param _newStake The new stake. - /// @param _alreadyTransferred Whether the PNKs were already transferred to/from the staking contract. /// @param _onError Whether to revert or return false on error. /// @return Whether the stake was successfully set or not. - function _setStake( - address _account, - uint96 _courtID, - uint256 _newStake, - bool _alreadyTransferred, - OnError _onError - ) internal returns (bool) { + function _setStake(address _account, uint96 _courtID, uint256 _newStake, OnError _onError) internal returns (bool) { if (_courtID == FORKING_COURT || _courtID > courts.length) { _stakingFailed(_onError, StakingResult.CannotStakeInThisCourt); // Staking directly into the forking court is not allowed. return false; @@ -1063,8 +1050,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = sortitionModule.setStake( _account, _courtID, - _newStake, - _alreadyTransferred + _newStake ); if (stakingResult != StakingResult.Successful) { _stakingFailed(_onError, stakingResult); diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index 795870e00..d95a159ee 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -135,15 +135,13 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, /// @param _account The address of the juror. /// @param _courtID The ID of the court. /// @param _newStake The new stake. - /// @param _alreadyTransferred True if the tokens were already transferred from juror. Only relevant for delayed stakes. /// @return pnkDeposit The amount of PNK to be deposited. /// @return pnkWithdrawal The amount of PNK to be withdrawn. /// @return stakingResult The result of the staking operation. function setStake( address _account, uint96 _courtID, - uint256 _newStake, - bool _alreadyTransferred + uint256 _newStake ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { Juror storage juror = jurors[_account]; uint256 currentStake = _stakeOf(_account, _courtID); @@ -154,11 +152,9 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, } if (_newStake >= currentStake) { - if (!_alreadyTransferred) { - pnkDeposit = _increaseStake(juror, _courtID, _newStake, currentStake); - } + pnkDeposit = _increaseStake(juror, _courtID, _newStake, currentStake); } else { - pnkWithdrawal += _decreaseStake(juror, _courtID, _newStake, currentStake); + pnkWithdrawal = _decreaseStake(juror, _courtID, _newStake, currentStake); } bool finished = false; @@ -257,7 +253,7 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, function setJurorInactive(address _account) external override onlyByCore { uint96[] memory courtIDs = getJurorCourtIDs(_account); for (uint256 j = courtIDs.length; j > 0; j--) { - core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0, false); + core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0); } } diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index dccf2fc3a..4e3b2126c 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -1240,7 +1240,7 @@ contract KlerosCoreTest is Test { // Note that functionality of this function was checked during delayed stakes execution vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector); vm.prank(governor); - core.setStakeBySortitionModule(staker1, GENERAL_COURT, 1000, false); + core.setStakeBySortitionModule(staker1, GENERAL_COURT, 1000); } function test_setStake_snapshotProxyCheck() public { From 8c5b67d823b21de5e5e1018ae3a60ccda48bf087 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 15 Jul 2025 23:25:13 +0200 Subject: [PATCH 05/12] refactor: renamed SM.setStake() into SM.validateStake(), SM.updateState() into SM.setStake() fixed natspec --- contracts/src/arbitration/KlerosCoreBase.sol | 4 +- .../src/arbitration/SortitionModuleBase.sol | 40 +++++++++++++------ .../src/arbitration/SortitionModuleNeo.sol | 4 +- .../interfaces/ISortitionModule.sol | 18 ++++----- .../university/KlerosCoreUniversity.sol | 2 +- .../university/SortitionModuleUniversity.sol | 4 +- 6 files changed, 43 insertions(+), 29 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 3b10dd662..85bc54ef5 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -1082,7 +1082,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable _stakingFailed(_onError, StakingResult.CannotStakeLessThanMinStake); // Staking less than the minimum stake is not allowed. return false; } - (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = sortitionModule.setStake( + (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = sortitionModule.validateStake( _account, _courtID, _newStake @@ -1105,7 +1105,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable return false; } } - sortitionModule.updateState(_account, _courtID, pnkDeposit, pnkWithdrawal, _newStake); + sortitionModule.setStake(_account, _courtID, pnkDeposit, pnkWithdrawal, _newStake); return true; } diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index 313a6d038..eccaabde1 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -259,29 +259,23 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// @param _randomNumber Random number returned by RNG contract. function notifyRandomNumber(uint256 _randomNumber) public override {} - /// @dev Sets the specified juror's stake in a court. - /// `O(n + p * log_k(j))` where - /// `n` is the number of courts the juror has staked in, - /// `p` is the depth of the court tree, - /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, - /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @dev Validate the specified juror's new stake for a court. + /// Note: no state changes should be made when returning stakingResult != Successful, otherwise delayed stakes might break invariants. /// @param _account The address of the juror. /// @param _courtID The ID of the court. /// @param _newStake The new stake. /// @return pnkDeposit The amount of PNK to be deposited. /// @return pnkWithdrawal The amount of PNK to be withdrawn. /// @return stakingResult The result of the staking operation. - function setStake( + function validateStake( address _account, uint96 _courtID, uint256 _newStake ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - (pnkDeposit, pnkWithdrawal, stakingResult) = _setStake(_account, _courtID, _newStake); + (pnkDeposit, pnkWithdrawal, stakingResult) = _validateStake(_account, _courtID, _newStake); } - /// @dev Sets the specified juror's stake in a court. - /// Note: no state changes should be made when returning stakingResult != Successful, otherwise delayed stakes might break invariants. - function _setStake( + function _validateStake( address _account, uint96 _courtID, uint256 _newStake @@ -322,14 +316,34 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); } - /// @dev Called by KC at the end of setStake flow. - function updateState( + /// @dev Update the state of the stakes, called by KC at the end of setStake flow. + /// `O(n + p * log_k(j))` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The address of the juror. + /// @param _courtID The ID of the court. + /// @param _pnkDeposit The amount of PNK to be deposited. + /// @param _pnkWithdrawal The amount of PNK to be withdrawn. + /// @param _newStake The new stake. + function setStake( address _account, uint96 _courtID, uint256 _pnkDeposit, uint256 _pnkWithdrawal, uint256 _newStake ) external override onlyByCore { + _setStake(_account, _courtID, _pnkDeposit, _pnkWithdrawal, _newStake); + } + + function _setStake( + address _account, + uint96 _courtID, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal, + uint256 _newStake + ) internal virtual { Juror storage juror = jurors[_account]; if (_pnkDeposit > 0) { uint256 currentStake = stakeOf(_account, _courtID); diff --git a/contracts/src/arbitration/SortitionModuleNeo.sol b/contracts/src/arbitration/SortitionModuleNeo.sol index c5a8300d7..2e822e380 100644 --- a/contracts/src/arbitration/SortitionModuleNeo.sol +++ b/contracts/src/arbitration/SortitionModuleNeo.sol @@ -84,7 +84,7 @@ contract SortitionModuleNeo is SortitionModuleBase { // * State Modifiers * // // ************************************* // - function _setStake( + function _validateStake( address _account, uint96 _courtID, uint256 _newStake @@ -108,6 +108,6 @@ contract SortitionModuleNeo is SortitionModuleBase { totalStaked -= stakeChange; } } - (pnkDeposit, pnkWithdrawal, stakingResult) = super._setStake(_account, _courtID, _newStake); + (pnkDeposit, pnkWithdrawal, stakingResult) = super._validateStake(_account, _courtID, _newStake); } } diff --git a/contracts/src/arbitration/interfaces/ISortitionModule.sol b/contracts/src/arbitration/interfaces/ISortitionModule.sol index 732c350ef..811a80664 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModule.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModule.sol @@ -14,12 +14,20 @@ interface ISortitionModule { function createTree(bytes32 _key, bytes memory _extraData) external; - function setStake( + function validateStake( address _account, uint96 _courtID, uint256 _newStake ) external returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult); + function setStake( + address _account, + uint96 _courtID, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal, + uint256 _newStake + ) external; + function setJurorInactive(address _account) external; function lockStake(address _account, uint256 _relativeAmount) external; @@ -49,12 +57,4 @@ interface ISortitionModule { function postDrawHook(uint256 _disputeID, uint256 _roundID) external; function withdrawLeftoverPNK(address _account) external; - - function updateState( - address _account, - uint96 _courtID, - uint256 _pnkDeposit, - uint256 _pnkWithdrawal, - uint256 _newStake - ) external; } diff --git a/contracts/src/arbitration/university/KlerosCoreUniversity.sol b/contracts/src/arbitration/university/KlerosCoreUniversity.sol index 67f310626..b787f795a 100644 --- a/contracts/src/arbitration/university/KlerosCoreUniversity.sol +++ b/contracts/src/arbitration/university/KlerosCoreUniversity.sol @@ -1047,7 +1047,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { _stakingFailed(_onError, StakingResult.CannotStakeLessThanMinStake); // Staking less than the minimum stake is not allowed. return false; } - (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = sortitionModule.setStake( + (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = sortitionModule.validateStake( _account, _courtID, _newStake diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index d95a159ee..ae20bf446 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -138,7 +138,7 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, /// @return pnkDeposit The amount of PNK to be deposited. /// @return pnkWithdrawal The amount of PNK to be withdrawn. /// @return stakingResult The result of the staking operation. - function setStake( + function validateStake( address _account, uint96 _courtID, uint256 _newStake @@ -313,7 +313,7 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, return jurors[_juror].stakedPnk > 0; } - function updateState( + function setStake( address _account, uint96 _courtID, uint256 _pnkDeposit, From e8c529e121f1fda7d94b1ed10f7c5d8cee325827 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 15 Jul 2025 23:51:26 +0200 Subject: [PATCH 06/12] fix: natspec --- contracts/src/arbitration/SortitionModuleBase.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index eccaabde1..3a9574989 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -302,7 +302,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed); } - // Current phase is Staking: set normal stakes or delayed stakes. + // Current phase is Staking: set stakes. if (_newStake >= currentStake) { pnkDeposit = _newStake - currentStake; } else { From 6c31770fff628bdd669026e3027d35afcd965a08 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 16 Jul 2025 00:01:18 +0200 Subject: [PATCH 07/12] fix: university contracts aligned with the base ones --- .../university/KlerosCoreUniversity.sol | 64 +++++-- .../university/SortitionModuleUniversity.sol | 169 ++++++++++-------- 2 files changed, 142 insertions(+), 91 deletions(-) diff --git a/contracts/src/arbitration/university/KlerosCoreUniversity.sol b/contracts/src/arbitration/university/KlerosCoreUniversity.sol index b787f795a..6a0951043 100644 --- a/contracts/src/arbitration/university/KlerosCoreUniversity.sol +++ b/contracts/src/arbitration/university/KlerosCoreUniversity.sol @@ -109,7 +109,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { event AppealDecision(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable); event Draw(address indexed _address, uint256 indexed _disputeID, uint256 _roundID, uint256 _voteID); event CourtCreated( - uint256 indexed _courtID, + uint96 indexed _courtID, uint96 indexed _parent, bool _hiddenVotes, uint256 _minStake, @@ -238,7 +238,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { court.timesPerPeriod = _timesPerPeriod; emit CourtCreated( - 1, + GENERAL_COURT, court.parent, _hiddenVotes, _courtParameters[0], @@ -361,7 +361,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { // Update the parent. courts[_parent].children.push(courtID); emit CourtCreated( - courtID, + uint96(courtID), _parent, _hiddenVotes, _minStake, @@ -468,6 +468,15 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { _setStake(_account, _courtID, _newStake, OnError.Return); } + /// @dev Transfers PNK to the juror by SortitionModule. + /// @param _account The account of the juror whose PNK to transfer. + /// @param _amount The amount to transfer. + function transferBySortitionModule(address _account, uint256 _amount) external { + if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); + // Note eligibility is checked in SortitionModule. + pinakion.safeTransfer(_account, _amount); + } + /// @inheritdoc IArbitratorV2 function createDispute( uint256 _numberOfChoices, @@ -562,7 +571,10 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { dispute.period = Period.appeal; emit AppealPossible(_disputeID, dispute.arbitrated); } else if (dispute.period == Period.appeal) { - if (block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)]) { + if ( + block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] && + !disputeKits[round.disputeKitID].isAppealFunded(_disputeID) + ) { revert AppealPeriodNotPassed(); } dispute.period = Period.execution; @@ -756,26 +768,25 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { // Fully coherent jurors won't be penalized. uint256 penalty = (round.pnkAtStakePerJuror * (ALPHA_DIVISOR - degreeOfCoherence)) / ALPHA_DIVISOR; - _params.pnkPenaltiesInRound += penalty; // Unlock the PNKs affected by the penalty address account = round.drawnJurors[_params.repartition]; sortitionModule.unlockStake(account, penalty); // Apply the penalty to the staked PNKs. - sortitionModule.penalizeStake(account, penalty); + (uint256 pnkBalance, uint256 availablePenalty) = sortitionModule.penalizeStake(account, penalty); + _params.pnkPenaltiesInRound += availablePenalty; emit TokenAndETHShift( account, _params.disputeID, _params.round, degreeOfCoherence, - -int256(penalty), + -int256(availablePenalty), 0, round.feeToken ); - - if (!disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { - // The juror is inactive, unstake them. + // Unstake the juror from all courts if he was inactive or his balance can't cover penalties anymore. + if (pnkBalance == 0 || !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { sortitionModule.setJurorInactive(account); } if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { @@ -826,11 +837,6 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { // Release the rest of the PNKs of the juror for this round. sortitionModule.unlockStake(account, pnkLocked); - // Give back the locked PNKs in case the juror fully unstaked earlier. - if (!sortitionModule.isJurorStaked(account)) { - pinakion.safeTransfer(account, pnkLocked); - } - // Transfer the rewards uint256 pnkReward = ((_params.pnkPenaltiesInRound / _params.coherentCount) * degreeOfCoherence) / ALPHA_DIVISOR; round.sumPnkRewardPaid += pnkReward; @@ -965,14 +971,34 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { (ruling, tied, overridden) = disputeKit.currentRuling(_disputeID); } + /// @dev Gets the round info for a specified dispute and round. + /// @dev This function must not be called from a non-view function because it returns a dynamic array which might be very large, theoretically exceeding the block gas limit. + /// @param _disputeID The ID of the dispute. + /// @param _round The round to get the info for. + /// @return round The round info. function getRoundInfo(uint256 _disputeID, uint256 _round) external view returns (Round memory) { return disputes[_disputeID].rounds[_round]; } + /// @dev Gets the PNK at stake per juror for a specified dispute and round. + /// @param _disputeID The ID of the dispute. + /// @param _round The round to get the info for. + /// @return pnkAtStakePerJuror The PNK at stake per juror. + function getPnkAtStakePerJuror(uint256 _disputeID, uint256 _round) external view returns (uint256) { + return disputes[_disputeID].rounds[_round].pnkAtStakePerJuror; + } + + /// @dev Gets the number of rounds for a specified dispute. + /// @param _disputeID The ID of the dispute. + /// @return The number of rounds. function getNumberOfRounds(uint256 _disputeID) external view returns (uint256) { return disputes[_disputeID].rounds.length; } + /// @dev Checks if a given dispute kit is supported by a given court. + /// @param _courtID The ID of the court to check the support for. + /// @param _disputeKitID The ID of the dispute kit to check the support for. + /// @return Whether the dispute kit is supported or not. function isSupported(uint96 _courtID, uint256 _disputeKitID) external view returns (bool) { return courts[_courtID].supportedDisputeKits[_disputeKitID]; } @@ -1039,7 +1065,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { /// @param _onError Whether to revert or return false on error. /// @return Whether the stake was successfully set or not. function _setStake(address _account, uint96 _courtID, uint256 _newStake, OnError _onError) internal returns (bool) { - if (_courtID == FORKING_COURT || _courtID > courts.length) { + if (_courtID == FORKING_COURT || _courtID >= courts.length) { _stakingFailed(_onError, StakingResult.CannotStakeInThisCourt); // Staking directly into the forking court is not allowed. return false; } @@ -1068,6 +1094,8 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { return false; } } + sortitionModule.setStake(_account, _courtID, pnkDeposit, pnkWithdrawal, _newStake); + return true; } @@ -1079,6 +1107,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { if (_result == StakingResult.CannotStakeInMoreCourts) revert StakingInTooManyCourts(); if (_result == StakingResult.CannotStakeInThisCourt) revert StakingNotPossibleInThisCourt(); if (_result == StakingResult.CannotStakeLessThanMinStake) revert StakingLessThanCourtMinStake(); + if (_result == StakingResult.CannotStakeZeroWhenNoStake) revert StakingZeroWhenNoStake(); } /// @dev Gets a court ID, the minimum number of jurors and an ID of a dispute kit from a specified extra data bytes array. @@ -1125,13 +1154,11 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { error SortitionModuleOnly(); error UnsuccessfulCall(); error InvalidDisputKitParent(); - error DepthLevelMax(); error MinStakeLowerThanParentCourt(); error UnsupportedDisputeKit(); error InvalidForkingCourtAsParent(); error WrongDisputeKitIndex(); error CannotDisableClassicDK(); - error ArraysLengthMismatch(); error StakingInTooManyCourts(); error StakingNotPossibleInThisCourt(); error StakingLessThanCourtMinStake(); @@ -1155,4 +1182,5 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { error TransferFailed(); error AllJurorsDrawn(); error NoJurorDrawn(); + error StakingZeroWhenNoStake(); } diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index ae20bf446..ccef301a9 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -60,6 +60,11 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, /// @param _unlock Whether the stake is locked or unlocked. event StakeLocked(address indexed _address, uint256 _relativeAmount, bool _unlock); + /// @dev Emitted when leftover PNK is withdrawn. + /// @param _account The account of the juror withdrawing PNK. + /// @param _amount The amount of PNK withdrawn. + event LeftoverPNKWithdrawn(address indexed _account, uint256 _amount); + // ************************************* // // * Function Modifiers * // // ************************************* // @@ -126,12 +131,8 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, /// @param _randomNumber Random number returned by RNG contract. function notifyRandomNumber(uint256 _randomNumber) public override {} - /// @dev Sets the specified juror's stake in a court. - /// `O(n + p * log_k(j))` where - /// `n` is the number of courts the juror has staked in, - /// `p` is the depth of the court tree, - /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, - /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @dev Validate the specified juror's new stake for a court. + /// Note: no state changes should be made when returning stakingResult != Successful, otherwise delayed stakes might break invariants. /// @param _account The address of the juror. /// @param _courtID The ID of the court. /// @param _newStake The new stake. @@ -142,19 +143,76 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, address _account, uint96 _courtID, uint256 _newStake - ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + ) + external + view + override + onlyByCore + returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) + { Juror storage juror = jurors[_account]; uint256 currentStake = _stakeOf(_account, _courtID); uint256 nbCourts = juror.courtIDs.length; - if (_newStake == 0 && (nbCourts >= MAX_STAKE_PATHS || currentStake == 0)) { + if (currentStake == 0 && nbCourts >= MAX_STAKE_PATHS) { return (0, 0, StakingResult.CannotStakeInMoreCourts); // Prevent staking beyond MAX_STAKE_PATHS but unstaking is always allowed. } + if (currentStake == 0 && _newStake == 0) { + return (0, 0, StakingResult.CannotStakeZeroWhenNoStake); // Forbid staking 0 amount when current stake is 0 to avoid flaky behaviour. + } + if (_newStake >= currentStake) { - pnkDeposit = _increaseStake(juror, _courtID, _newStake, currentStake); + pnkDeposit = _newStake - currentStake; } else { - pnkWithdrawal = _decreaseStake(juror, _courtID, _newStake, currentStake); + pnkWithdrawal = currentStake - _newStake; + // Ensure locked tokens remain in the contract. They can only be released during Execution. + uint256 possibleWithdrawal = juror.stakedPnk > juror.lockedPnk ? juror.stakedPnk - juror.lockedPnk : 0; + if (pnkWithdrawal > possibleWithdrawal) { + pnkWithdrawal = possibleWithdrawal; + } + } + return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); + } + + /// @dev Update the state of the stakes, called by KC at the end of setStake flow. + /// `O(n + p * log_k(j))` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The address of the juror. + /// @param _courtID The ID of the court. + /// @param _pnkDeposit The amount of PNK to be deposited. + /// @param _pnkWithdrawal The amount of PNK to be withdrawn. + /// @param _newStake The new stake. + function setStake( + address _account, + uint96 _courtID, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal, + uint256 _newStake + ) external override onlyByCore { + Juror storage juror = jurors[_account]; + uint256 currentStake = _stakeOf(_account, _courtID); + if (_pnkDeposit > 0) { + if (currentStake == 0) { + juror.courtIDs.push(_courtID); + } + // Increase juror's balance by deposited amount. + juror.stakedPnk += _pnkDeposit; + } else { + juror.stakedPnk -= _pnkWithdrawal; + if (_newStake == 0) { + // Cleanup + for (uint256 i = juror.courtIDs.length; i > 0; i--) { + if (juror.courtIDs[i - 1] == _courtID) { + juror.courtIDs[i - 1] = juror.courtIDs[juror.courtIDs.length - 1]; + juror.courtIDs.pop(); + break; + } + } + } } bool finished = false; @@ -170,55 +228,6 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, } } emit StakeSet(_account, _courtID, _newStake, juror.stakedPnk); - return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); - } - - function _increaseStake( - Juror storage juror, - uint96 _courtID, - uint256 _newStake, - uint256 _currentStake - ) internal returns (uint256 transferredAmount) { - // Stake increase - // When stakedPnk becomes lower than lockedPnk count the locked tokens in when transferring tokens from juror. - // (E.g. stakedPnk = 0, lockedPnk = 150) which can happen if the juror unstaked fully while having some tokens locked. - uint256 previouslyLocked = (juror.lockedPnk >= juror.stakedPnk) ? juror.lockedPnk - juror.stakedPnk : 0; // underflow guard - transferredAmount = (_newStake >= _currentStake + previouslyLocked) // underflow guard - ? _newStake - _currentStake - previouslyLocked - : 0; - if (_currentStake == 0) { - juror.courtIDs.push(_courtID); - } - // stakedPnk can become async with _currentStake (e.g. after penalty). - juror.stakedPnk = (juror.stakedPnk >= _currentStake) ? juror.stakedPnk - _currentStake + _newStake : _newStake; - } - - function _decreaseStake( - Juror storage juror, - uint96 _courtID, - uint256 _newStake, - uint256 _currentStake - ) internal returns (uint256 transferredAmount) { - // Stakes can be partially delayed only when stake is increased. - // Stake decrease: make sure locked tokens always stay in the contract. They can only be released during Execution. - if (juror.stakedPnk >= _currentStake - _newStake + juror.lockedPnk) { - // We have enough pnk staked to afford withdrawal while keeping locked tokens. - transferredAmount = _currentStake - _newStake; - } else if (juror.stakedPnk >= juror.lockedPnk) { - // Can't afford withdrawing the current stake fully. Take whatever is available while keeping locked tokens. - transferredAmount = juror.stakedPnk - juror.lockedPnk; - } - if (_newStake == 0) { - for (uint256 i = juror.courtIDs.length; i > 0; i--) { - if (juror.courtIDs[i - 1] == _courtID) { - juror.courtIDs[i - 1] = juror.courtIDs[juror.courtIDs.length - 1]; - juror.courtIDs.pop(); - break; - } - } - } - // stakedPnk can become async with _currentStake (e.g. after penalty). - juror.stakedPnk = (juror.stakedPnk >= _currentStake) ? juror.stakedPnk - _currentStake + _newStake : _newStake; } function lockStake(address _account, uint256 _relativeAmount) external override onlyByCore { @@ -236,11 +245,18 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, uint256 _relativeAmount ) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) { Juror storage juror = jurors[_account]; - if (juror.stakedPnk >= _relativeAmount) { + uint256 stakedPnk = juror.stakedPnk; + + if (stakedPnk >= _relativeAmount) { + availablePenalty = _relativeAmount; juror.stakedPnk -= _relativeAmount; } else { - juror.stakedPnk = 0; // stakedPnk might become lower after manual unstaking, but lockedPnk will always cover the difference. + availablePenalty = stakedPnk; + juror.stakedPnk = 0; } + + pnkBalance = juror.stakedPnk; + return (pnkBalance, availablePenalty); } /// @dev Unstakes the inactive juror from all courts. @@ -257,7 +273,25 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, } } - function withdrawLeftoverPNK(address _account) external override {} + /// @dev Gives back the locked PNKs in case the juror fully unstaked earlier. + /// Note that since locked and staked PNK are async it is possible for the juror to have positive staked PNK balance + /// while having 0 stake in courts and 0 locked tokens (eg. when the juror fully unstaked during dispute and later got his tokens unlocked). + /// In this case the juror can use this function to withdraw the leftover tokens. + /// Also note that if the juror has some leftover PNK while not fully unstaked he'll have to manually unstake from all courts to trigger this function. + /// @param _account The juror whose PNK to withdraw. + function withdrawLeftoverPNK(address _account) external override { + Juror storage juror = jurors[_account]; + // Can withdraw the leftover PNK if fully unstaked, has no tokens locked and has positive balance. + // This withdrawal can't be triggered by calling setStake() in KlerosCore because current stake is technically 0, thus it is done via separate function. + if (juror.stakedPnk > 0 && juror.courtIDs.length == 0 && juror.lockedPnk == 0) { + uint256 amount = juror.stakedPnk; + juror.stakedPnk = 0; + core.transferBySortitionModule(_account, amount); + emit LeftoverPNKWithdrawn(_account, amount); + } else { + revert("Not eligible for withdrawal."); + } + } // ************************************* // // * Public Views * // @@ -266,9 +300,6 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, /// @dev Draw an ID from a tree using a number. /// Note that this function reverts if the sum of all values in the tree is 0. /// @return drawnAddress The drawn address. - /// `O(k * log_k(n))` where - /// `k` is the maximum number of children per node in the tree, - /// and `n` is the maximum number of nodes ever appended. function draw(bytes32, uint256, uint256) public view override returns (address drawnAddress) { drawnAddress = transientJuror; } @@ -313,14 +344,6 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, return jurors[_juror].stakedPnk > 0; } - function setStake( - address _account, - uint96 _courtID, - uint256 _pnkDeposit, - uint256 _pnkWithdrawal, - uint256 _newStake - ) external {} - // ************************************* // // * Internal * // // ************************************* // From d50b8a42a6beac22de9858e585842c85ae0bb800 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 16 Jul 2025 01:57:32 +0200 Subject: [PATCH 08/12] fix: removal of the unused instant staking events --- .../src/arbitration/SortitionModuleBase.sol | 18 ----- subgraph/core/tests/sortition-module-utils.ts | 76 ++----------------- 2 files changed, 8 insertions(+), 86 deletions(-) diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index 3a9574989..abd7aa768 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -81,24 +81,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// @param _amountAllCourts The amount of tokens staked in all courts. event StakeSet(address indexed _address, uint256 _courtID, uint256 _amount, uint256 _amountAllCourts); - /// @notice DEPRECATED Emitted when a juror's stake is delayed and tokens are not transferred yet. - /// @param _address The address of the juror. - /// @param _courtID The ID of the court. - /// @param _amount The amount of tokens staked in the court. - event StakeDelayedNotTransferred(address indexed _address, uint256 _courtID, uint256 _amount); - - /// @notice DEPRECATED Emitted when a juror's stake is delayed and tokens are already deposited. - /// @param _address The address of the juror. - /// @param _courtID The ID of the court. - /// @param _amount The amount of tokens staked in the court. - event StakeDelayedAlreadyTransferredDeposited(address indexed _address, uint256 _courtID, uint256 _amount); - - /// @notice DEPRECATED Emitted when a juror's stake is delayed and tokens are already withdrawn. - /// @param _address The address of the juror. - /// @param _courtID The ID of the court. - /// @param _amount The amount of tokens withdrawn. - event StakeDelayedAlreadyTransferredWithdrawn(address indexed _address, uint96 indexed _courtID, uint256 _amount); - /// @notice Emitted when a juror's stake is delayed. /// @param _address The address of the juror. /// @param _courtID The ID of the court. diff --git a/subgraph/core/tests/sortition-module-utils.ts b/subgraph/core/tests/sortition-module-utils.ts index a6d590679..f57f2fbed 100644 --- a/subgraph/core/tests/sortition-module-utils.ts +++ b/subgraph/core/tests/sortition-module-utils.ts @@ -1,77 +1,17 @@ import { newMockEvent } from "matchstick-as"; import { ethereum, BigInt, Address } from "@graphprotocol/graph-ts"; -import { - StakeDelayedAlreadyTransferredDeposited, - StakeDelayedAlreadyTransferredWithdrawn, - StakeDelayedNotTransferred, - StakeLocked, - StakeSet, -} from "../generated/SortitionModule/SortitionModule"; +import { StakeDelayed, StakeLocked, StakeSet } from "../generated/SortitionModule/SortitionModule"; -export function createStakeDelayedAlreadyTransferredDepositedEvent( - _address: Address, - _courtID: BigInt, - _amount: BigInt -): StakeDelayedAlreadyTransferredDeposited { - let stakeDelayedAlreadyTransferredDepositedEvent: StakeDelayedAlreadyTransferredDeposited = newMockEvent(); +export function createStakeDelayedEvent(_address: Address, _courtID: BigInt, _amount: BigInt): StakeDelayed { + let StakeDelayed: StakeDelayed = newMockEvent(); - stakeDelayedAlreadyTransferredDepositedEvent.parameters = new Array(); + StakeDelayed.parameters = new Array(); - stakeDelayedAlreadyTransferredDepositedEvent.parameters.push( - new ethereum.EventParam("_address", ethereum.Value.fromAddress(_address)) - ); - stakeDelayedAlreadyTransferredDepositedEvent.parameters.push( - new ethereum.EventParam("_courtID", ethereum.Value.fromUnsignedBigInt(_courtID)) - ); - stakeDelayedAlreadyTransferredDepositedEvent.parameters.push( - new ethereum.EventParam("_amount", ethereum.Value.fromUnsignedBigInt(_amount)) - ); - - return stakeDelayedAlreadyTransferredDepositedEvent; -} - -export function createStakeDelayedAlreadyTransferredWithdrawnEvent( - _address: Address, - _courtID: BigInt, - _amount: BigInt -): StakeDelayedAlreadyTransferredWithdrawn { - let stakeDelayedAlreadyTransferredWithdrawnEvent = newMockEvent(); - - stakeDelayedAlreadyTransferredWithdrawnEvent.parameters = new Array(); - - stakeDelayedAlreadyTransferredWithdrawnEvent.parameters.push( - new ethereum.EventParam("_address", ethereum.Value.fromAddress(_address)) - ); - stakeDelayedAlreadyTransferredWithdrawnEvent.parameters.push( - new ethereum.EventParam("_courtID", ethereum.Value.fromUnsignedBigInt(_courtID)) - ); - stakeDelayedAlreadyTransferredWithdrawnEvent.parameters.push( - new ethereum.EventParam("_amount", ethereum.Value.fromUnsignedBigInt(_amount)) - ); - - return stakeDelayedAlreadyTransferredWithdrawnEvent; -} - -export function createStakeDelayedNotTransferredEvent( - _address: Address, - _courtID: BigInt, - _amount: BigInt -): StakeDelayedNotTransferred { - let stakeDelayedNotTransferredEvent = newMockEvent(); - - stakeDelayedNotTransferredEvent.parameters = new Array(); - - stakeDelayedNotTransferredEvent.parameters.push( - new ethereum.EventParam("_address", ethereum.Value.fromAddress(_address)) - ); - stakeDelayedNotTransferredEvent.parameters.push( - new ethereum.EventParam("_courtID", ethereum.Value.fromUnsignedBigInt(_courtID)) - ); - stakeDelayedNotTransferredEvent.parameters.push( - new ethereum.EventParam("_amount", ethereum.Value.fromUnsignedBigInt(_amount)) - ); + StakeDelayed.parameters.push(new ethereum.EventParam("_address", ethereum.Value.fromAddress(_address))); + StakeDelayed.parameters.push(new ethereum.EventParam("_courtID", ethereum.Value.fromUnsignedBigInt(_courtID))); + StakeDelayed.parameters.push(new ethereum.EventParam("_amount", ethereum.Value.fromUnsignedBigInt(_amount))); - return stakeDelayedNotTransferredEvent; + return StakeDelayed; } export function createStakeLockedEvent(_address: Address, _relativeAmount: BigInt, _unlock: boolean): StakeLocked { From 2e71da97a55b79b688ba81091eb5598560e2c056 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 16 Jul 2025 02:20:56 +0200 Subject: [PATCH 09/12] feat: emit event LeftoverPNK when PNK becomes available for withdrawal during unlock --- .../src/arbitration/SortitionModuleBase.sol | 36 +++++++++++++------ .../interfaces/ISortitionModule.sol | 2 ++ .../university/SortitionModuleUniversity.sol | 27 +++++++++----- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index abd7aa768..d73c99f9f 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -93,6 +93,11 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// @param _unlock Whether the stake is locked or unlocked. event StakeLocked(address indexed _address, uint256 _relativeAmount, bool _unlock); + /// @dev Emitted when leftover PNK is available. + /// @param _account The account of the juror. + /// @param _amount The amount of PNK available. + event LeftoverPNK(address indexed _account, uint256 _amount); + /// @dev Emitted when leftover PNK is withdrawn. /// @param _account The account of the juror withdrawing PNK. /// @param _amount The amount of PNK withdrawn. @@ -370,8 +375,14 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } function unlockStake(address _account, uint256 _relativeAmount) external override onlyByCore { - jurors[_account].lockedPnk -= _relativeAmount; + Juror storage juror = jurors[_account]; + juror.lockedPnk -= _relativeAmount; emit StakeLocked(_account, _relativeAmount, true); + + uint256 amount = getJurorLeftoverPNK(_account); + if (amount > 0) { + emit LeftoverPNK(_account, amount); + } } function penalizeStake( @@ -414,17 +425,13 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// Also note that if the juror has some leftover PNK while not fully unstaked he'll have to manually unstake from all courts to trigger this function. /// @param _account The juror whose PNK to withdraw. function withdrawLeftoverPNK(address _account) external override { - Juror storage juror = jurors[_account]; // Can withdraw the leftover PNK if fully unstaked, has no tokens locked and has positive balance. // This withdrawal can't be triggered by calling setStake() in KlerosCore because current stake is technically 0, thus it is done via separate function. - if (juror.stakedPnk > 0 && juror.courtIDs.length == 0 && juror.lockedPnk == 0) { - uint256 amount = juror.stakedPnk; - juror.stakedPnk = 0; - core.transferBySortitionModule(_account, amount); - emit LeftoverPNKWithdrawn(_account, amount); - } else { - revert("Not eligible for withdrawal."); - } + uint256 amount = getJurorLeftoverPNK(_account); + require(amount > 0, "Not eligible for withdrawal."); + jurors[_account].stakedPnk = 0; + core.transferBySortitionModule(_account, amount); + emit LeftoverPNKWithdrawn(_account, amount); } // ************************************* // @@ -524,6 +531,15 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr return jurors[_juror].stakedPnk > 0; } + function getJurorLeftoverPNK(address _juror) public view override returns (uint256) { + Juror storage juror = jurors[_juror]; + if (juror.courtIDs.length == 0 && juror.lockedPnk == 0) { + return juror.stakedPnk; + } else { + return 0; + } + } + // ************************************* // // * Internal * // // ************************************* // diff --git a/contracts/src/arbitration/interfaces/ISortitionModule.sol b/contracts/src/arbitration/interfaces/ISortitionModule.sol index 811a80664..c4019fe98 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModule.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModule.sol @@ -52,6 +52,8 @@ interface ISortitionModule { function isJurorStaked(address _juror) external view returns (bool); + function getJurorLeftoverPNK(address _juror) external view returns (uint256); + function createDisputeHook(uint256 _disputeID, uint256 _roundID) external; function postDrawHook(uint256 _disputeID, uint256 _roundID) external; diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index ccef301a9..2fac8e279 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -60,6 +60,11 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, /// @param _unlock Whether the stake is locked or unlocked. event StakeLocked(address indexed _address, uint256 _relativeAmount, bool _unlock); + /// @dev Emitted when leftover PNK is available. + /// @param _account The account of the juror. + /// @param _amount The amount of PNK available. + event LeftoverPNK(address indexed _account, uint256 _amount); + /// @dev Emitted when leftover PNK is withdrawn. /// @param _account The account of the juror withdrawing PNK. /// @param _amount The amount of PNK withdrawn. @@ -280,17 +285,13 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, /// Also note that if the juror has some leftover PNK while not fully unstaked he'll have to manually unstake from all courts to trigger this function. /// @param _account The juror whose PNK to withdraw. function withdrawLeftoverPNK(address _account) external override { - Juror storage juror = jurors[_account]; // Can withdraw the leftover PNK if fully unstaked, has no tokens locked and has positive balance. // This withdrawal can't be triggered by calling setStake() in KlerosCore because current stake is technically 0, thus it is done via separate function. - if (juror.stakedPnk > 0 && juror.courtIDs.length == 0 && juror.lockedPnk == 0) { - uint256 amount = juror.stakedPnk; - juror.stakedPnk = 0; - core.transferBySortitionModule(_account, amount); - emit LeftoverPNKWithdrawn(_account, amount); - } else { - revert("Not eligible for withdrawal."); - } + uint256 amount = getJurorLeftoverPNK(_account); + require(amount > 0, "Not eligible for withdrawal."); + jurors[_account].stakedPnk = 0; + core.transferBySortitionModule(_account, amount); + emit LeftoverPNKWithdrawn(_account, amount); } // ************************************* // @@ -344,6 +345,14 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, return jurors[_juror].stakedPnk > 0; } + function getJurorLeftoverPNK(address _juror) public view override returns (uint256) { + Juror storage juror = jurors[_juror]; + if (juror.courtIDs.length == 0 && juror.lockedPnk == 0) { + return juror.stakedPnk; + } + return 0; + } + // ************************************* // // * Internal * // // ************************************* // From aadc2d4e8281b0219abfce04a69e15f5c37ee07b Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Fri, 18 Jul 2025 18:29:11 +1000 Subject: [PATCH 10/12] fix(KC): staking test update --- contracts/test/arbitration/draw.ts | 12 +-- contracts/test/arbitration/staking-neo.ts | 102 +++++++++++----------- contracts/test/arbitration/staking.ts | 94 ++++++++++---------- contracts/test/foundry/KlerosCore.t.sol | 4 + 4 files changed, 103 insertions(+), 109 deletions(-) diff --git a/contracts/test/arbitration/draw.ts b/contracts/test/arbitration/draw.ts index 68317612b..1ce33bd5b 100644 --- a/contracts/test/arbitration/draw.ts +++ b/contracts/test/arbitration/draw.ts @@ -254,7 +254,7 @@ describe("Draw Benchmark", async () => { await sortitionModule.getJurorBalance(wallet.address, PARENT_COURT), "Drawn jurors have a locked stake in the parent court" ).to.deep.equal([ - 0, // totalStaked + locked, // totalStaked won't go lower than locked amount locked, // totalLocked 0, // stakedInCourt 0, // nbOfCourts @@ -263,7 +263,7 @@ describe("Draw Benchmark", async () => { await sortitionModule.getJurorBalance(wallet.address, CHILD_COURT), "No locked stake in the child court" ).to.deep.equal([ - 0, // totalStaked + locked, // totalStaked won't go lower than locked amount locked, // totalLocked 0, // stakedInCourt 0, // nbOfCourts @@ -361,7 +361,7 @@ describe("Draw Benchmark", async () => { await sortitionModule.getJurorBalance(wallet.address, PARENT_COURT), "No locked stake in the parent court" ).to.deep.equal([ - 0, // totalStaked + locked, // totalStaked won't go lower than locked amount locked, // totalLocked 0, // stakedInCourt 0, // nbOfCourts @@ -370,7 +370,7 @@ describe("Draw Benchmark", async () => { await sortitionModule.getJurorBalance(wallet.address, CHILD_COURT), "Drawn jurors have a locked stake in the child court" ).to.deep.equal([ - 0, // totalStaked + locked, // totalStaked won't go lower than locked amount locked, // totalLocked 0, // stakedInCourt 0, // nbOfCourts @@ -427,7 +427,7 @@ describe("Draw Benchmark", async () => { await sortitionModule.getJurorBalance(wallet.address, PARENT_COURT), "No locked stake in the parent court" ).to.deep.equal([ - 0, // totalStaked + locked, // totalStaked won't go lower than locked amount locked, // totalLocked 0, // stakedInCourt 0, // nbOfCourts @@ -436,7 +436,7 @@ describe("Draw Benchmark", async () => { await sortitionModule.getJurorBalance(wallet.address, CHILD_COURT), "Drawn jurors have a locked stake in the child court" ).to.deep.equal([ - 0, // totalStaked + locked, // totalStaked won't go lower than locked amount locked, // totalLocked 0, // stakedInCourt 0, // nbOfCourts diff --git a/contracts/test/arbitration/staking-neo.ts b/contracts/test/arbitration/staking-neo.ts index 2c1918f21..85b0dc884 100644 --- a/contracts/test/arbitration/staking-neo.ts +++ b/contracts/test/arbitration/staking-neo.ts @@ -223,14 +223,14 @@ describe("Staking", async () => { it("Should be able to unstake", async () => { expect(await core.connect(juror).setStake(1, PNK(0))) - .to.emit(sortition, "StakeDelayedNotTransferred") + .to.emit(sortition, "StakeDelayed") .withArgs(juror.address, 1, PNK(0)) .to.not.emit(sortition, "StakeSet"); expect(await sortition.totalStaked()).to.be.equal(PNK(1000)); await drawAndReachStakingPhaseFromGenerating(); expect(await sortition.executeDelayedStakes(10)) .to.emit(sortition, "StakeSet") - .withArgs(juror.address, 1, PNK(0), PNK(0)); + .withArgs(juror.address, 1, PNK(0), PNK(1000)); // Staked amount won't go lower than locked amount. }); }); }); @@ -272,7 +272,7 @@ describe("Staking", async () => { await core.connect(juror).setStake(1, PNK(1000)); await createDisputeAndReachGeneratingPhaseFromStaking(); expect(await core.connect(juror).setStake(1, PNK(2000))) - .to.emit(sortition, "StakeDelayedAlreadyTransferredDeposited") + .to.emit(sortition, "StakeDelayed") .withArgs(juror.address, 1, PNK(2000)) .to.not.emit(sortition, "StakeSet"); expect(await sortition.totalStaked()).to.be.equal(PNK(1000)); @@ -334,7 +334,7 @@ describe("Staking", async () => { it("Should be able to stake exactly maxTotalStaked", async () => { await pnk.connect(juror).approve(core.target, PNK(1000)); await expect(await core.connect(juror).setStake(1, PNK(1000))) - .to.emit(sortition, "StakeDelayedAlreadyTransferredDeposited") + .to.emit(sortition, "StakeDelayed") .withArgs(juror.address, 1, PNK(1000)); expect(await sortition.totalStaked()).to.be.equal(PNK(2000)); // Not updated until the delayed stake is executed await drawAndReachStakingPhaseFromGenerating(); @@ -424,26 +424,26 @@ describe("Staking", async () => { }); describe("When stake is increased", () => { - it("Should transfer PNK but delay the stake increase", async () => { + it("Should delay the stake increase", async () => { expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); await pnk.approve(core.target, PNK(1000)); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(3000))) - .to.emit(sortition, "StakeDelayedAlreadyTransferredDeposited") + .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(3000)); - expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(5000), 0, PNK(2000), 2]); // stake does not change + expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]); // stake does not change }); - it("Should transfer some PNK out of the juror's account", async () => { - expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore - PNK(1000)); // PNK is transferred out of the juror's account + it("Should not transfer PNK", async () => { + expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore); }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(1); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); - expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), true]); + expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), false]); }); }); @@ -457,9 +457,7 @@ describe("Staking", async () => { await expect(await sortition.executeDelayedStakes(10)) .to.emit(sortition, "StakeSet") .withArgs(deployer, 2, PNK(3000), PNK(5000)) - .to.not.emit(sortition, "StakeDelayedNotTransferred") - .to.not.emit(sortition, "StakeDelayedAlreadyTransferredDeposited") - .to.not.emit(sortition, "StakeDelayedAlreadyTransferredWithdrawn"); + .to.not.emit(sortition, "StakeDelayed"); expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([ PNK(5000), PNK(300), // we're the only juror so we are drawn 3 times @@ -470,11 +468,11 @@ describe("Staking", async () => { expect(await sortition.delayedStakeReadIndex()).to.be.equal(2); expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 2nd delayed stake got deleted - expect(await sortition.latestDelayedStakeIndex(deployer, 1)).to.be.equal(0); // no delayed stakes left + expect(await sortition.latestDelayedStakeIndex(deployer, 1)).to.be.equal(0); // Deprecated. Always 0 }); - it("Should not transfer any PNK", async () => { - expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore); // No PNK transfer + it("Should transfer PNK after delayed stake execution", async () => { + expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore - PNK(1000)); // No PNK transfer }); }); }); @@ -496,9 +494,9 @@ describe("Staking", async () => { it("Should delay the stake decrease", async () => { expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(1000))) - .to.emit(sortition, "StakeDelayedNotTransferred") + .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(1000)); expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]); // stake unchanged, delayed }); @@ -508,7 +506,7 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(1); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(1000), false]); @@ -535,7 +533,7 @@ describe("Staking", async () => { expect(await sortition.delayedStakeReadIndex()).to.be.equal(2); expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 2nd delayed stake got deleted - expect(await sortition.latestDelayedStakeIndex(deployer, 1)).to.be.equal(0); // no delayed stakes left + expect(await sortition.latestDelayedStakeIndex(deployer, 1)).to.be.equal(0); // Deprecated. Always 0 }); it("Should withdraw some PNK", async () => { @@ -561,9 +559,9 @@ describe("Staking", async () => { it("Should delay the stake decrease", async () => { expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(1000))) - .to.emit(sortition, "StakeDelayedNotTransferred") + .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(1000)); expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]); // stake unchanged, delayed }); @@ -573,7 +571,7 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(1); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(1000), false]); @@ -583,11 +581,10 @@ describe("Staking", async () => { describe("When stake is increased back to the previous amount", () => { it("Should delay the stake increase", async () => { balanceBefore = await pnk.balanceOf(deployer); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(1); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(2000))) - .to.emit(sortition, "StakeDelayedNotTransferred") - .withArgs(deployer, 2, PNK(2000)) - .to.not.emit(sortition, "StakeDelayedAlreadyTransferredWithdrawn"); + .to.emit(sortition, "StakeDelayed") + .withArgs(deployer, 2, PNK(2000)); expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]); // stake unchanged, delayed }); @@ -596,10 +593,10 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(2); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 expect(await sortition.delayedStakeWriteIndex()).to.be.equal(2); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); - expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted + expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(1000), false]); expect(await sortition.delayedStakes(2)).to.be.deep.equal([deployer, 2, PNK(2000), false]); }); }); @@ -611,9 +608,12 @@ describe("Staking", async () => { }); it("Should execute the delayed stakes but the stakes should remain the same", async () => { + await pnk.approve(core.target, PNK(1000)); await expect(await sortition.executeDelayedStakes(10)) .to.emit(sortition, "StakeSet") - .withArgs(deployer, 2, PNK(2000), PNK(4000)); + .withArgs(deployer, 2, PNK(1000), PNK(3000)) + .to.emit(sortition, "StakeSet") + .withArgs(deployer, 2, PNK(2000), PNK(4000)); // 2nd delayed stake will override the first one expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([ PNK(4000), PNK(300), // we're the only juror so we are drawn 3 times @@ -624,7 +624,7 @@ describe("Staking", async () => { expect(await sortition.delayedStakeReadIndex()).to.be.equal(3); expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 2nd delayed stake got deleted - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // no delayed stakes left + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 }); it("Should not transfer any PNK", async () => { @@ -647,50 +647,48 @@ describe("Staking", async () => { }); describe("When stake is increased", () => { - it("Should transfer PNK but delay the stake increase", async () => { + it("Should delay the stake increase", async () => { expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); await pnk.approve(core.target, PNK(1000)); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(3000))) - .to.emit(sortition, "StakeDelayedAlreadyTransferredDeposited") + .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(3000)); - expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(5000), 0, PNK(2000), 2]); // stake does not change + expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]); // stake does not change }); - it("Should transfer some PNK out of the juror's account", async () => { - expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore - PNK(1000)); // PNK is transferred out of the juror's account + it("Should not transfer PNK", async () => { + expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore); }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(1); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); - expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), true]); + expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), false]); }); }); describe("When stake is decreased back to the previous amount", () => { it("Should cancel out the stake decrease back", async () => { balanceBefore = await pnk.balanceOf(deployer); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(1); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(2000))) - .to.emit(sortition, "StakeDelayedAlreadyTransferredWithdrawn") - .withArgs(deployer, 2, PNK(1000)) - .to.emit(sortition, "StakeDelayedNotTransferred") + .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(2000)); - expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]); // stake has changed immediately + expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]); // stake is unchanged }); it("Should transfer back some PNK to the juror", async () => { - expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore + PNK(1000)); // PNK is sent back to the juror + expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore); // PNK balance left unchanged }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(2); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 expect(await sortition.delayedStakeWriteIndex()).to.be.equal(2); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); - expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted + expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), false]); expect(await sortition.delayedStakes(2)).to.be.deep.equal([deployer, 2, PNK(2000), false]); }); }); @@ -705,9 +703,7 @@ describe("Staking", async () => { await expect(sortition.executeDelayedStakes(10)) .to.emit(await sortition, "StakeSet") .withArgs(deployer, 2, PNK(2000), PNK(4000)) - .to.not.emit(sortition, "StakeDelayedNotTransferred") - .to.not.emit(sortition, "StakeDelayedAlreadyTransferredDeposited") - .to.not.emit(sortition, "StakeDelayedAlreadyTransferredWithdrawn"); + .to.not.emit(sortition, "StakeDelayed"); expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([ PNK(4000), PNK(300), // we're the only juror so we are drawn 3 times @@ -718,7 +714,7 @@ describe("Staking", async () => { expect(await sortition.delayedStakeReadIndex()).to.be.equal(3); expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 2nd delayed stake got deleted - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // no delayed stakes left + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 }); it("Should not transfer any PNK", async () => { diff --git a/contracts/test/arbitration/staking.ts b/contracts/test/arbitration/staking.ts index 9ff51fd84..4d0262c22 100644 --- a/contracts/test/arbitration/staking.ts +++ b/contracts/test/arbitration/staking.ts @@ -81,26 +81,26 @@ describe("Staking", async () => { }); describe("When stake is increased", () => { - it("Should transfer PNK but delay the stake increase", async () => { + it("Should delay the stake increase", async () => { expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); await pnk.approve(core.target, PNK(1000)); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(3000))) - .to.emit(sortition, "StakeDelayedAlreadyTransferredDeposited") + .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(3000)); - expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(5000), 0, PNK(2000), 2]); // stake does not change + expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]); // stake does not change }); - it("Should transfer some PNK out of the juror's account", async () => { - expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore - PNK(1000)); // PNK is transferred out of the juror's account + it("Should not transfer PNK", async () => { + expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore); }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(1); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); - expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), true]); + expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), false]); }); }); @@ -114,9 +114,7 @@ describe("Staking", async () => { await expect(sortition.executeDelayedStakes(10)) .to.emit(sortition, "StakeSet") .withArgs(deployer, 2, PNK(3000), PNK(5000)) - .to.not.emit(sortition, "StakeDelayedNotTransferred") - .to.not.emit(sortition, "StakeDelayedAlreadyTransferredDeposited") - .to.not.emit(sortition, "StakeDelayedAlreadyTransferredWithdrawn"); + .to.not.emit(sortition, "StakeDelayed"); expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([ PNK(5000), PNK(300), // we're the only juror so we are drawn 3 times @@ -127,11 +125,11 @@ describe("Staking", async () => { expect(await sortition.delayedStakeReadIndex()).to.be.equal(2); expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 2nd delayed stake got deleted - expect(await sortition.latestDelayedStakeIndex(deployer, 1)).to.be.equal(0); // no delayed stakes left + expect(await sortition.latestDelayedStakeIndex(deployer, 1)).to.be.equal(0); // Deprecated. Always 0 }); - it("Should not transfer any PNK", async () => { - expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore); // No PNK transfer + it("Should transfer PNK after delayed stake execution", async () => { + expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore - PNK(1000)); // No PNK transfer }); }); }); @@ -151,9 +149,9 @@ describe("Staking", async () => { it("Should delay the stake decrease", async () => { expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(1000))) - .to.emit(sortition, "StakeDelayedNotTransferred") + .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(1000)); expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]); // stake unchanged, delayed }); @@ -163,7 +161,7 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(1); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(1000), false]); @@ -190,7 +188,7 @@ describe("Staking", async () => { expect(await sortition.delayedStakeReadIndex()).to.be.equal(2); expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 2nd delayed stake got deleted - expect(await sortition.latestDelayedStakeIndex(deployer, 1)).to.be.equal(0); // no delayed stakes left + expect(await sortition.latestDelayedStakeIndex(deployer, 1)).to.be.equal(0); // Deprecated. Always 0 }); it("Should withdraw some PNK", async () => { @@ -214,9 +212,9 @@ describe("Staking", async () => { it("Should delay the stake decrease", async () => { expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(1000))) - .to.emit(sortition, "StakeDelayedNotTransferred") + .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(1000)); expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]); // stake unchanged, delayed }); @@ -226,7 +224,7 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(1); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(1000), false]); @@ -236,11 +234,8 @@ describe("Staking", async () => { describe("When stake is increased back to the previous amount", () => { it("Should delay the stake increase", async () => { balanceBefore = await pnk.balanceOf(deployer); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(1); - await expect(core.setStake(2, PNK(2000))) - .to.emit(sortition, "StakeDelayedNotTransferred") - .withArgs(deployer, 2, PNK(2000)) - .to.not.emit(sortition, "StakeDelayedAlreadyTransferredWithdrawn"); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 + await expect(core.setStake(2, PNK(2000))).to.emit(sortition, "StakeDelayed"); expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]); // stake unchanged, delayed }); @@ -249,10 +244,10 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(2); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 expect(await sortition.delayedStakeWriteIndex()).to.be.equal(2); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); - expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted + expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(1000), false]); expect(await sortition.delayedStakes(2)).to.be.deep.equal([deployer, 2, PNK(2000), false]); }); }); @@ -264,9 +259,12 @@ describe("Staking", async () => { }); it("Should execute the delayed stakes but the stakes should remain the same", async () => { + await pnk.approve(core.target, PNK(1000)); await expect(sortition.executeDelayedStakes(10)) .to.emit(sortition, "StakeSet") - .withArgs(deployer, 2, PNK(2000), PNK(4000)); + .withArgs(deployer, 2, PNK(1000), PNK(3000)) + .to.emit(sortition, "StakeSet") + .withArgs(deployer, 2, PNK(2000), PNK(4000)); // 2nd delayed stake will override the first one expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([ PNK(4000), PNK(300), // we're the only juror so we are drawn 3 times @@ -277,7 +275,7 @@ describe("Staking", async () => { expect(await sortition.delayedStakeReadIndex()).to.be.equal(3); expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 2nd delayed stake got deleted - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // no delayed stakes left + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 }); it("Should not transfer any PNK", async () => { @@ -298,50 +296,48 @@ describe("Staking", async () => { }); describe("When stake is increased", () => { - it("Should transfer PNK but delay the stake increase", async () => { + it("Should delay the stake increase", async () => { expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); await pnk.approve(core.target, PNK(1000)); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(3000))) - .to.emit(sortition, "StakeDelayedAlreadyTransferredDeposited") + .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(3000)); - expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(5000), 0, PNK(2000), 2]); // stake does not change + expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]); // stake does not change }); - it("Should transfer some PNK out of the juror's account", async () => { - expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore - PNK(1000)); // PNK is transferred out of the juror's account + it("Should not transfer PNK", async () => { + expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore); }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(1); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); - expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), true]); + expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), false]); }); }); describe("When stake is decreased back to the previous amount", () => { it("Should cancel out the stake decrease back", async () => { balanceBefore = await pnk.balanceOf(deployer); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(1); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(2000))) - .to.emit(sortition, "StakeDelayedAlreadyTransferredWithdrawn") - .withArgs(deployer, 2, PNK(1000)) - .to.emit(sortition, "StakeDelayedNotTransferred") + .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(2000)); - expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]); // stake has changed immediately + expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]); // stake is unchanged }); it("Should transfer back some PNK to the juror", async () => { - expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore + PNK(1000)); // PNK is sent back to the juror + expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore); // PNK balance left unchanged }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(2); + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 expect(await sortition.delayedStakeWriteIndex()).to.be.equal(2); expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); - expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted + expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), false]); expect(await sortition.delayedStakes(2)).to.be.deep.equal([deployer, 2, PNK(2000), false]); }); }); @@ -356,9 +352,7 @@ describe("Staking", async () => { await expect(sortition.executeDelayedStakes(10)) .to.emit(sortition, "StakeSet") .withArgs(deployer, 2, PNK(2000), PNK(4000)) - .to.not.emit(sortition, "StakeDelayedNotTransferred") - .to.not.emit(sortition, "StakeDelayedAlreadyTransferredDeposited") - .to.not.emit(sortition, "StakeDelayedAlreadyTransferredWithdrawn"); + .to.not.emit(sortition, "StakeDelayed"); expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([ PNK(4000), PNK(300), // we're the only juror so we are drawn 3 times @@ -369,7 +363,7 @@ describe("Staking", async () => { expect(await sortition.delayedStakeReadIndex()).to.be.equal(3); expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 2nd delayed stake got deleted - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // no delayed stakes left + expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 }); it("Should not transfer any PNK", async () => { diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index 4e3b2126c..bdb909a4e 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -2558,6 +2558,8 @@ contract KlerosCoreTest is Test { vm.expectRevert(bytes("Not eligible for withdrawal.")); sortitionModule.withdrawLeftoverPNK(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.LeftoverPNK(staker1, 1000); core.execute(disputeID, 0, 6); (totalStaked, totalLocked, , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); @@ -2577,6 +2579,8 @@ contract KlerosCoreTest is Test { vm.prank(governor); core.transferBySortitionModule(staker1, 1000); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.LeftoverPNKWithdrawn(staker1, 1000); sortitionModule.withdrawLeftoverPNK(staker1); (totalStaked, , , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); From 29ab108a660389c722e811a4581d9b8215d595fe Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Fri, 18 Jul 2025 14:27:47 +0200 Subject: [PATCH 11/12] docs: custom natspec cleanup --- contracts/src/arbitration/SortitionModule.sol | 8 -------- contracts/src/arbitration/SortitionModuleBase.sol | 8 -------- contracts/src/arbitration/SortitionModuleNeo.sol | 8 -------- contracts/src/test/SortitionModuleMock.sol | 8 -------- 4 files changed, 32 deletions(-) diff --git a/contracts/src/arbitration/SortitionModule.sol b/contracts/src/arbitration/SortitionModule.sol index 3c076791f..fe35f83c0 100644 --- a/contracts/src/arbitration/SortitionModule.sol +++ b/contracts/src/arbitration/SortitionModule.sol @@ -1,13 +1,5 @@ // SPDX-License-Identifier: MIT -/** - * @custom:authors: [@epiqueras, @unknownunknown1, @jaybuidl, @shotaronowhere] - * @custom:reviewers: [] - * @custom:auditors: [] - * @custom:bounties: [] - * @custom:deployments: [] - */ - pragma solidity 0.8.24; import {SortitionModuleBase, KlerosCore, RNG} from "./SortitionModuleBase.sol"; diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index d73c99f9f..7e9e8f45d 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -1,13 +1,5 @@ // SPDX-License-Identifier: MIT -/** - * @custom:authors: [@epiqueras, @unknownunknown1, @jaybuidl, @shotaronowhere] - * @custom:reviewers: [] - * @custom:auditors: [] - * @custom:bounties: [] - * @custom:deployments: [] - */ - pragma solidity 0.8.24; import {KlerosCore} from "./KlerosCore.sol"; diff --git a/contracts/src/arbitration/SortitionModuleNeo.sol b/contracts/src/arbitration/SortitionModuleNeo.sol index 2e822e380..0e005810b 100644 --- a/contracts/src/arbitration/SortitionModuleNeo.sol +++ b/contracts/src/arbitration/SortitionModuleNeo.sol @@ -1,13 +1,5 @@ // SPDX-License-Identifier: MIT -/** - * @custom:authors: [@jaybuidl, @unknownunknown1] - * @custom:reviewers: [] - * @custom:auditors: [] - * @custom:bounties: [] - * @custom:deployments: [] - */ - pragma solidity 0.8.24; import {SortitionModuleBase, KlerosCore, RNG, StakingResult} from "./SortitionModuleBase.sol"; diff --git a/contracts/src/test/SortitionModuleMock.sol b/contracts/src/test/SortitionModuleMock.sol index bfe911dfe..0c80d99dd 100644 --- a/contracts/src/test/SortitionModuleMock.sol +++ b/contracts/src/test/SortitionModuleMock.sol @@ -1,13 +1,5 @@ // SPDX-License-Identifier: MIT -/** - * @custom:authors: [unknownunknown1] - * @custom:reviewers: [] - * @custom:auditors: [] - * @custom:bounties: [] - * @custom:deployments: [] - */ - pragma solidity 0.8.24; import "../arbitration/SortitionModule.sol"; From d9c7ec6ec56d9a3e73de9df5e6be436feeeedfe6 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Fri, 18 Jul 2025 14:56:11 +0200 Subject: [PATCH 12/12] fix: subgraph abi migrations update --- .../abi-migrations/KlerosCoreNeo.json | 33 ++++- .../abi-migrations/SortitionModuleNeo.json | 132 +++++++++++++++--- subgraph/core/abi-migrations/KlerosCore.json | 82 +++++++---- .../core/abi-migrations/SortitionModule.json | 132 +++++++++++++++--- 4 files changed, 301 insertions(+), 78 deletions(-) diff --git a/subgraph/core-neo/abi-migrations/KlerosCoreNeo.json b/subgraph/core-neo/abi-migrations/KlerosCoreNeo.json index 565d577ee..505b50298 100644 --- a/subgraph/core-neo/abi-migrations/KlerosCoreNeo.json +++ b/subgraph/core-neo/abi-migrations/KlerosCoreNeo.json @@ -1454,7 +1454,13 @@ } ], "name": "draw", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "nbDrawnJurors", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, @@ -1806,7 +1812,7 @@ }, { "inputs": [], - "name": "initialize3", + "name": "initialize5", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -1973,11 +1979,6 @@ "internalType": "uint256", "name": "_newStake", "type": "uint256" - }, - { - "internalType": "bool", - "name": "_alreadyTransferred", - "type": "bool" } ], "name": "setStakeBySortitionModule", @@ -1998,6 +1999,24 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "transferBySortitionModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "unpause", diff --git a/subgraph/core-neo/abi-migrations/SortitionModuleNeo.json b/subgraph/core-neo/abi-migrations/SortitionModuleNeo.json index f7a756723..45981621f 100644 --- a/subgraph/core-neo/abi-migrations/SortitionModuleNeo.json +++ b/subgraph/core-neo/abi-migrations/SortitionModuleNeo.json @@ -60,6 +60,25 @@ "name": "Initialized", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_account", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "LeftoverPNK", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -393,6 +412,11 @@ "internalType": "uint256", "name": "stake", "type": "uint256" + }, + { + "internalType": "bool", + "name": "alreadyTransferred", + "type": "bool" } ], "stateMutability": "view", @@ -511,6 +535,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_juror", + "type": "address" + } + ], + "name": "getJurorLeftoverPNK", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "governor", @@ -635,6 +678,30 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "jurorAccount", + "type": "address" + }, + { + "internalType": "uint96", + "name": "courtId", + "type": "uint96" + } + ], + "name": "latestDelayedStakeIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -877,38 +944,22 @@ }, { "internalType": "uint256", - "name": "_newStake", - "type": "uint256" - }, - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "name": "setStake", - "outputs": [ - { - "internalType": "uint256", - "name": "pnkDeposit", + "name": "_pnkDeposit", "type": "uint256" }, { "internalType": "uint256", - "name": "pnkWithdrawal", + "name": "_pnkWithdrawal", "type": "uint256" }, { "internalType": "uint256", - "name": "oldStake", + "name": "_newStake", "type": "uint256" - }, - { - "internalType": "enum StakingResult", - "name": "stakingResult", - "type": "uint8" } ], + "name": "setStake", + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -1009,6 +1060,45 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_account", + "type": "address" + }, + { + "internalType": "uint96", + "name": "_courtID", + "type": "uint96" + }, + { + "internalType": "uint256", + "name": "_newStake", + "type": "uint256" + } + ], + "name": "validateStake", + "outputs": [ + { + "internalType": "uint256", + "name": "pnkDeposit", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pnkWithdrawal", + "type": "uint256" + }, + { + "internalType": "enum StakingResult", + "name": "stakingResult", + "type": "uint8" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", diff --git a/subgraph/core/abi-migrations/KlerosCore.json b/subgraph/core/abi-migrations/KlerosCore.json index 5662a72bc..e7cd08ed5 100644 --- a/subgraph/core/abi-migrations/KlerosCore.json +++ b/subgraph/core/abi-migrations/KlerosCore.json @@ -1,12 +1,9 @@ { "abi": [ { - "stateMutability": "payable", - "type": "fallback" - }, - { - "stateMutability": "payable", - "type": "receive" + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" }, { "inputs": [], @@ -1387,7 +1384,13 @@ } ], "name": "draw", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "nbDrawnJurors", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, @@ -1524,6 +1527,30 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_disputeID", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_round", + "type": "uint256" + } + ], + "name": "getPnkAtStakePerJuror", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -1710,7 +1737,7 @@ }, { "inputs": [], - "name": "initialize3", + "name": "initialize5", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -1864,11 +1891,6 @@ "internalType": "uint256", "name": "_newStake", "type": "uint256" - }, - { - "internalType": "bool", - "name": "_alreadyTransferred", - "type": "bool" } ], "name": "setStakeBySortitionModule", @@ -1889,6 +1911,24 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "transferBySortitionModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "unpause", @@ -1926,22 +1966,6 @@ ], "stateMutability": "view", "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_implementation", - "type": "address" - }, - { - "internalType": "bytes", - "name": "_data", - "type": "bytes" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" } ] } diff --git a/subgraph/core/abi-migrations/SortitionModule.json b/subgraph/core/abi-migrations/SortitionModule.json index 859f4ae9f..244546c90 100644 --- a/subgraph/core/abi-migrations/SortitionModule.json +++ b/subgraph/core/abi-migrations/SortitionModule.json @@ -60,6 +60,25 @@ "name": "Initialized", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_account", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "LeftoverPNK", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -367,6 +386,11 @@ "internalType": "uint256", "name": "stake", "type": "uint256" + }, + { + "internalType": "bool", + "name": "alreadyTransferred", + "type": "bool" } ], "stateMutability": "view", @@ -485,6 +509,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_juror", + "type": "address" + } + ], + "name": "getJurorLeftoverPNK", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "governor", @@ -599,6 +642,30 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "jurorAccount", + "type": "address" + }, + { + "internalType": "uint96", + "name": "courtId", + "type": "uint96" + } + ], + "name": "latestDelayedStakeIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -815,38 +882,22 @@ }, { "internalType": "uint256", - "name": "_newStake", - "type": "uint256" - }, - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "name": "setStake", - "outputs": [ - { - "internalType": "uint256", - "name": "pnkDeposit", + "name": "_pnkDeposit", "type": "uint256" }, { "internalType": "uint256", - "name": "pnkWithdrawal", + "name": "_pnkWithdrawal", "type": "uint256" }, { "internalType": "uint256", - "name": "oldStake", + "name": "_newStake", "type": "uint256" - }, - { - "internalType": "enum StakingResult", - "name": "stakingResult", - "type": "uint8" } ], + "name": "setStake", + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -934,6 +985,45 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_account", + "type": "address" + }, + { + "internalType": "uint96", + "name": "_courtID", + "type": "uint96" + }, + { + "internalType": "uint256", + "name": "_newStake", + "type": "uint256" + } + ], + "name": "validateStake", + "outputs": [ + { + "internalType": "uint256", + "name": "pnkDeposit", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pnkWithdrawal", + "type": "uint256" + }, + { + "internalType": "enum StakingResult", + "name": "stakingResult", + "type": "uint8" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version",