diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 6a435489b..85bc54ef5 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -463,22 +463,25 @@ 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. - /// @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); + } + + /// @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 +777,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 +846,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,16 +1071,9 @@ 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, - 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; @@ -1092,15 +1082,16 @@ 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, - _alreadyTransferred + _newStake ); - 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)) { @@ -1114,6 +1105,8 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable return false; } } + sortitionModule.setStake(_account, _courtID, pnkDeposit, pnkWithdrawal, _newStake); + return true; } 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/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 edb10edf1..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"; @@ -25,13 +17,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 +31,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 +60,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,23 +73,11 @@ 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 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 StakeDelayedNotTransferred(address indexed _address, uint256 _courtID, uint256 _amount); - - /// @notice 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. - /// @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); + 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. @@ -112,6 +85,16 @@ 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. + event LeftoverPNKWithdrawn(address indexed _account, uint256 _amount); + // ************************************* // // * Constructor * // // ************************************* // @@ -237,18 +220,8 @@ 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); + delete delayedStakes[i]; } delayedStakeReadIndex = newDelayedStakeReadIndex; } @@ -265,35 +238,26 @@ 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. - /// @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( + function validateStake( 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, _alreadyTransferred); + (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 `succeeded` = false, otherwise delayed stakes might break invariants. - function _setStake( + function _validateStake( 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); @@ -307,33 +271,78 @@ 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); - } - return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); + emit StakeDelayed(_account, _courtID, _newStake); + return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed); } - // Current phase is Staking: set normal stakes or delayed stakes (which may have been already transferred). + // Current phase is Staking: set stakes. if (_newStake >= currentStake) { - if (!_alreadyTransferred) { - pnkDeposit = _increaseStake(juror, _courtID, _newStake, currentStake); + pnkDeposit = _newStake - currentStake; + } else { + 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 { + _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); + if (currentStake == 0) { + juror.courtIDs.push(_courtID); + } + // Increase juror's balance by deposited amount. + juror.stakedPnk += _pnkDeposit; } else { - pnkWithdrawal += _decreaseStake(juror, _courtID, _newStake, currentStake); + 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. @@ -350,95 +359,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } } emit StakeSet(_account, _courtID, _newStake, juror.stakedPnk); - 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 { @@ -447,17 +367,33 @@ 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(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. @@ -470,10 +406,26 @@ 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); } } + /// @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 { + // 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. + uint256 amount = getJurorLeftoverPNK(_account); + require(amount > 0, "Not eligible for withdrawal."); + jurors[_account].stakedPnk = 0; + core.transferBySortitionModule(_account, amount); + emit LeftoverPNKWithdrawn(_account, amount); + } + // ************************************* // // * Public Views * // // ************************************* // @@ -571,6 +523,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/SortitionModuleNeo.sol b/contracts/src/arbitration/SortitionModuleNeo.sol index 2e60307d2..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"; @@ -84,17 +76,16 @@ contract SortitionModuleNeo is SortitionModuleBase { // * State Modifiers * // // ************************************* // - function _setStake( + function _validateStake( 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; 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); } @@ -109,11 +100,6 @@ contract SortitionModuleNeo is SortitionModuleBase { totalStaked -= stakeChange; } } - (pnkDeposit, pnkWithdrawal, stakingResult) = super._setStake( - _account, - _courtID, - _newStake, - _alreadyTransferred - ); + (pnkDeposit, pnkWithdrawal, stakingResult) = super._validateStake(_account, _courtID, _newStake); } } 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..c4019fe98 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModule.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModule.sol @@ -14,20 +14,30 @@ interface ISortitionModule { function createTree(bytes32 _key, bytes memory _extraData) external; - function setStake( + function validateStake( address _account, uint96 _courtID, - uint256 _newStake, - bool _alreadyTransferred + 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; 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; @@ -42,7 +52,11 @@ 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; + + function withdrawLeftoverPNK(address _account) external; } diff --git a/contracts/src/arbitration/university/KlerosCoreUniversity.sol b/contracts/src/arbitration/university/KlerosCoreUniversity.sol index e7182a809..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, @@ -456,22 +456,25 @@ 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, 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(); - _setStake(_account, _courtID, _newStake, _alreadyTransferred, OnError.Return); + // Note eligibility is checked in SortitionModule. + pinakion.safeTransfer(_account, _amount); } /// @inheritdoc IArbitratorV2 @@ -568,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; @@ -762,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) { @@ -832,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; @@ -971,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]; } @@ -1042,17 +1062,10 @@ 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) { - if (_courtID == FORKING_COURT || _courtID > courts.length) { + 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; } @@ -1060,11 +1073,10 @@ 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, - _alreadyTransferred + _newStake ); if (stakingResult != StakingResult.Successful) { _stakingFailed(_onError, stakingResult); @@ -1082,6 +1094,8 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { return false; } } + sortitionModule.setStake(_account, _courtID, pnkDeposit, pnkWithdrawal, _newStake); + return true; } @@ -1093,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. @@ -1139,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(); @@ -1169,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 b178c8b75..2fac8e279 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -60,6 +60,16 @@ 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. + event LeftoverPNKWithdrawn(address indexed _account, uint256 _amount); + // ************************************* // // * Function Modifiers * // // ************************************* // @@ -126,39 +136,88 @@ 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. - /// @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( + function validateStake( address _account, uint96 _courtID, - uint256 _newStake, - bool _alreadyTransferred - ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + uint256 _newStake + ) + 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) { - if (!_alreadyTransferred) { - pnkDeposit = _increaseStake(juror, _courtID, _newStake, currentStake); + pnkDeposit = _newStake - currentStake; + } else { + 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 { - pnkWithdrawal += _decreaseStake(juror, _courtID, _newStake, currentStake); + 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; @@ -174,55 +233,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 { @@ -235,13 +245,23 @@ 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) { + 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. @@ -254,10 +274,26 @@ 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); } } + /// @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 { + // 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. + uint256 amount = getJurorLeftoverPNK(_account); + require(amount > 0, "Not eligible for withdrawal."); + jurors[_account].stakedPnk = 0; + core.transferBySortitionModule(_account, amount); + emit LeftoverPNKWithdrawn(_account, amount); + } + // ************************************* // // * Public Views * // // ************************************* // @@ -265,9 +301,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; } @@ -312,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 * // // ************************************* // 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, 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"; 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 58ea8ce60..bdb909a4e 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,59 +1236,11 @@ 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); vm.prank(governor); - core.setStakeBySortitionModule(staker1, GENERAL_COURT, 1000, false); + core.setStakeBySortitionModule(staker1, GENERAL_COURT, 1000); } function test_setStake_snapshotProxyCheck() public { @@ -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,52 @@ 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); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.LeftoverPNK(staker1, 1000); 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); + + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.LeftoverPNKWithdrawn(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 { 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 b65294575..45981621f 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,7 +76,7 @@ "type": "uint256" } ], - "name": "StakeDelayedAlreadyTransferredDeposited", + "name": "LeftoverPNK", "type": "event" }, { @@ -104,15 +85,9 @@ { "indexed": true, "internalType": "address", - "name": "_address", + "name": "_account", "type": "address" }, - { - "indexed": false, - "internalType": "uint256", - "name": "_courtID", - "type": "uint256" - }, { "indexed": false, "internalType": "uint256", @@ -120,32 +95,20 @@ "type": "uint256" } ], - "name": "StakeDelayedAlreadyTransferred", + "name": "LeftoverPNKWithdrawn", "type": "event" }, { "anonymous": false, "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_address", - "type": "address" - }, - { - "indexed": true, - "internalType": "uint96", - "name": "_courtID", - "type": "uint96" - }, { "indexed": false, - "internalType": "uint256", - "name": "_amount", - "type": "uint256" + "internalType": "enum ISortitionModule.Phase", + "name": "_phase", + "type": "uint8" } ], - "name": "StakeDelayedAlreadyTransferredWithdrawn", + "name": "NewPhase", "type": "event" }, { @@ -158,10 +121,10 @@ "type": "address" }, { - "indexed": false, - "internalType": "uint256", + "indexed": true, + "internalType": "uint96", "name": "_courtID", - "type": "uint256" + "type": "uint96" }, { "indexed": false, @@ -170,7 +133,7 @@ "type": "uint256" } ], - "name": "StakeDelayedNotTransferred", + "name": "StakeDelayed", "type": "event" }, { @@ -572,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", @@ -824,7 +806,18 @@ } ], "name": "penalizeStake", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "pnkBalance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "availablePenalty", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, @@ -951,33 +944,22 @@ }, { "internalType": "uint256", - "name": "_newStake", + "name": "_pnkDeposit", "type": "uint256" }, - { - "internalType": "bool", - "name": "_alreadyTransferred", - "type": "bool" - } - ], - "name": "setStake", - "outputs": [ { "internalType": "uint256", - "name": "pnkDeposit", + "name": "_pnkWithdrawal", "type": "uint256" }, { "internalType": "uint256", - "name": "pnkWithdrawal", + "name": "_newStake", "type": "uint256" - }, - { - "internalType": "enum StakingResult", - "name": "stakingResult", - "type": "uint8" } ], + "name": "setStake", + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -1078,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", @@ -1090,6 +1111,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/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 d9ba2ca9e..244546c90 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,7 +76,7 @@ "type": "uint256" } ], - "name": "StakeDelayedAlreadyTransferredDeposited", + "name": "LeftoverPNK", "type": "event" }, { @@ -107,15 +85,9 @@ { "indexed": true, "internalType": "address", - "name": "_address", + "name": "_account", "type": "address" }, - { - "indexed": false, - "internalType": "uint256", - "name": "_courtID", - "type": "uint256" - }, { "indexed": false, "internalType": "uint256", @@ -123,32 +95,20 @@ "type": "uint256" } ], - "name": "StakeDelayedAlreadyTransferred", + "name": "LeftoverPNKWithdrawn", "type": "event" }, { "anonymous": false, "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_address", - "type": "address" - }, - { - "indexed": true, - "internalType": "uint96", - "name": "_courtID", - "type": "uint96" - }, { "indexed": false, - "internalType": "uint256", - "name": "_amount", - "type": "uint256" + "internalType": "enum ISortitionModule.Phase", + "name": "_phase", + "type": "uint8" } ], - "name": "StakeDelayedAlreadyTransferredWithdrawn", + "name": "NewPhase", "type": "event" }, { @@ -161,10 +121,10 @@ "type": "address" }, { - "indexed": false, - "internalType": "uint256", + "indexed": true, + "internalType": "uint96", "name": "_courtID", - "type": "uint256" + "type": "uint96" }, { "indexed": false, @@ -173,7 +133,7 @@ "type": "uint256" } ], - "name": "StakeDelayedNotTransferred", + "name": "StakeDelayed", "type": "event" }, { @@ -549,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", @@ -765,7 +744,18 @@ } ], "name": "penalizeStake", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "pnkBalance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "availablePenalty", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, @@ -892,33 +882,22 @@ }, { "internalType": "uint256", - "name": "_newStake", + "name": "_pnkDeposit", "type": "uint256" }, - { - "internalType": "bool", - "name": "_alreadyTransferred", - "type": "bool" - } - ], - "name": "setStake", - "outputs": [ { "internalType": "uint256", - "name": "pnkDeposit", + "name": "_pnkWithdrawal", "type": "uint256" }, { "internalType": "uint256", - "name": "pnkWithdrawal", + "name": "_newStake", "type": "uint256" - }, - { - "internalType": "enum StakingResult", - "name": "stakingResult", - "type": "uint8" } ], + "name": "setStake", + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -1006,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", @@ -1023,17 +1041,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/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 { 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": [