diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 6a435489b..97d52efee 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -470,15 +470,23 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _account The account whose stake is being set. /// @param _courtID The ID of the court. /// @param _newStake The new stake. - /// @param _alreadyTransferred Whether the PNKs have already been transferred to the contract. function setStakeBySortitionModule( address _account, uint96 _courtID, uint256 _newStake, - bool _alreadyTransferred + bool /*_alreadyTransferred*/ ) external { if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); - _setStake(_account, _courtID, _newStake, _alreadyTransferred, OnError.Return); + _setStake(_account, _courtID, _newStake, false, OnError.Return); // alreadyTransferred is unused and DEPRECATED. + } + + /// @dev Transfers PNK to the juror by SortitionModule. + /// @param _account The account of the juror whose PNK to transfer. + /// @param _amount The amount to transfer. + function transferBySortitionModule(address _account, uint256 _amount) external { + if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); + // Note eligibility is checked in SortitionModule. + pinakion.safeTransfer(_account, _amount); } /// @inheritdoc IArbitratorV2 @@ -774,26 +782,25 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // Fully coherent jurors won't be penalized. uint256 penalty = (round.pnkAtStakePerJuror * (ALPHA_DIVISOR - degreeOfCoherence)) / ALPHA_DIVISOR; - _params.pnkPenaltiesInRound += penalty; // Unlock the PNKs affected by the penalty address account = round.drawnJurors[_params.repartition]; sortitionModule.unlockStake(account, penalty); // Apply the penalty to the staked PNKs. - sortitionModule.penalizeStake(account, penalty); + (uint256 pnkBalance, uint256 availablePenalty) = sortitionModule.penalizeStake(account, penalty); + _params.pnkPenaltiesInRound += availablePenalty; emit TokenAndETHShift( account, _params.disputeID, _params.round, degreeOfCoherence, - -int256(penalty), + -int256(availablePenalty), 0, round.feeToken ); - - if (!disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { - // The juror is inactive, unstake them. + // Unstake the juror from all courts if he was inactive or his balance can't cover penalties anymore. + if (pnkBalance == 0 || !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { sortitionModule.setJurorInactive(account); } if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { @@ -844,11 +851,6 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // Release the rest of the PNKs of the juror for this round. sortitionModule.unlockStake(account, pnkLocked); - // Give back the locked PNKs in case the juror fully unstaked earlier. - if (!sortitionModule.isJurorStaked(account)) { - pinakion.safeTransfer(account, pnkLocked); - } - // Transfer the rewards uint256 pnkReward = ((_params.pnkPenaltiesInRound / _params.coherentCount) * degreeOfCoherence) / ALPHA_DIVISOR; round.sumPnkRewardPaid += pnkReward; @@ -1074,14 +1076,13 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _account The account to set the stake for. /// @param _courtID The ID of the court to set the stake for. /// @param _newStake The new stake. - /// @param _alreadyTransferred Whether the PNKs were already transferred to/from the staking contract. /// @param _onError Whether to revert or return false on error. /// @return Whether the stake was successfully set or not. function _setStake( address _account, uint96 _courtID, uint256 _newStake, - bool _alreadyTransferred, + bool /*_alreadyTransferred*/, OnError _onError ) internal returns (bool) { if (_courtID == FORKING_COURT || _courtID >= courts.length) { @@ -1096,11 +1097,13 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable _account, _courtID, _newStake, - _alreadyTransferred + false // Unused parameter. ); - if (stakingResult != StakingResult.Successful) { + if (stakingResult != StakingResult.Successful && stakingResult != StakingResult.Delayed) { _stakingFailed(_onError, stakingResult); return false; + } else if (stakingResult == StakingResult.Delayed) { + return true; } if (pnkDeposit > 0) { if (!pinakion.safeTransferFrom(_account, address(this), pnkDeposit)) { @@ -1114,6 +1117,8 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable return false; } } + sortitionModule.updateState(_account, _courtID, pnkDeposit, pnkWithdrawal, _newStake); + return true; } diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index edb10edf1..2a71588e0 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -25,13 +25,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // * Enums / Structs * // // ************************************* // - enum PreStakeHookResult { - ok, // Correct phase. All checks are passed. - stakeDelayedAlreadyTransferred, // Wrong phase but stake is increased, so transfer the tokens without updating the drawing chance. - stakeDelayedNotTransferred, // Wrong phase and stake is decreased. Delay the token transfer and drawing chance update. - failed // Checks didn't pass. Do no changes. - } - struct SortitionSumTree { uint256 K; // The maximum number of children per node. // We use this to keep track of vacant positions in the tree after removing a leaf. This is for keeping the tree as balanced as possible without spending gas on moving nodes around. @@ -46,13 +39,13 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr address account; // The address of the juror. uint96 courtID; // The ID of the court. uint256 stake; // The new stake. - bool alreadyTransferred; // True if tokens were already transferred before delayed stake's execution. + bool alreadyTransferred; // DEPRECATED. True if tokens were already transferred before delayed stake's execution. } struct Juror { uint96[] courtIDs; // The IDs of courts where the juror's stake path ends. A stake path is a path from the general court to a court the juror directly staked in using `_setStake`. uint256 stakedPnk; // The juror's total amount of tokens staked in subcourts. Reflects actual pnk balance. - uint256 lockedPnk; // The juror's total amount of tokens locked in disputes. Can reflect actual pnk balance when stakedPnk are fully withdrawn. + uint256 lockedPnk; // The juror's total amount of tokens locked in disputes. } // ************************************* // @@ -75,7 +68,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr mapping(bytes32 treeHash => SortitionSumTree) sortitionSumTrees; // The mapping trees by keys. mapping(address account => Juror) public jurors; // The jurors. mapping(uint256 => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking. - mapping(address jurorAccount => mapping(uint96 courtId => uint256)) public latestDelayedStakeIndex; // Maps the juror to its latest delayed stake. If there is already a delayed stake for this juror then it'll be replaced. latestDelayedStakeIndex[juror][courtID]. + mapping(address jurorAccount => mapping(uint96 courtId => uint256)) public latestDelayedStakeIndex; // DEPRECATED. Maps the juror to its latest delayed stake. If there is already a delayed stake for this juror then it'll be replaced. latestDelayedStakeIndex[juror][courtID]. // ************************************* // // * Events * // @@ -88,30 +81,41 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// @param _amountAllCourts The amount of tokens staked in all courts. event StakeSet(address indexed _address, uint256 _courtID, uint256 _amount, uint256 _amountAllCourts); - /// @notice Emitted when a juror's stake is delayed and tokens are not transferred yet. + /// @notice DEPRECATED Emitted when a juror's stake is delayed and tokens are not transferred yet. /// @param _address The address of the juror. /// @param _courtID The ID of the court. /// @param _amount The amount of tokens staked in the court. event StakeDelayedNotTransferred(address indexed _address, uint256 _courtID, uint256 _amount); - /// @notice Emitted when a juror's stake is delayed and tokens are already deposited. + /// @notice DEPRECATED Emitted when a juror's stake is delayed and tokens are already deposited. /// @param _address The address of the juror. /// @param _courtID The ID of the court. /// @param _amount The amount of tokens staked in the court. event StakeDelayedAlreadyTransferredDeposited(address indexed _address, uint256 _courtID, uint256 _amount); - /// @notice Emitted when a juror's stake is delayed and tokens are already withdrawn. + /// @notice DEPRECATED Emitted when a juror's stake is delayed and tokens are already withdrawn. /// @param _address The address of the juror. /// @param _courtID The ID of the court. /// @param _amount The amount of tokens withdrawn. event StakeDelayedAlreadyTransferredWithdrawn(address indexed _address, uint96 indexed _courtID, uint256 _amount); + /// @notice Emitted when a juror's stake is delayed. + /// @param _address The address of the juror. + /// @param _courtID The ID of the court. + /// @param _amount The amount of tokens staked in the court. + event StakeDelayed(address indexed _address, uint96 indexed _courtID, uint256 _amount); + /// @notice Emitted when a juror's stake is locked. /// @param _address The address of the juror. /// @param _relativeAmount The amount of tokens locked. /// @param _unlock Whether the stake is locked or unlocked. event StakeLocked(address indexed _address, uint256 _relativeAmount, bool _unlock); + /// @dev Emitted when leftover PNK is withdrawn. + /// @param _account The account of the juror withdrawing PNK. + /// @param _amount The amount of PNK withdrawn. + event LeftoverPNKWithdrawn(address indexed _account, uint256 _amount); + // ************************************* // // * Constructor * // // ************************************* // @@ -237,18 +241,13 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) { DelayedStake storage delayedStake = delayedStakes[i]; - // Delayed stake could've been manually removed already. In this case simply move on to the next item. - if (delayedStake.account != address(0)) { - // Nullify the index so the delayed stake won't get deleted before its own execution. - delete latestDelayedStakeIndex[delayedStake.account][delayedStake.courtID]; - core.setStakeBySortitionModule( - delayedStake.account, - delayedStake.courtID, - delayedStake.stake, - delayedStake.alreadyTransferred - ); - delete delayedStakes[i]; - } + core.setStakeBySortitionModule( + delayedStake.account, + delayedStake.courtID, + delayedStake.stake, + false // Unused parameter. + ); + delete delayedStakes[i]; } delayedStakeReadIndex = newDelayedStakeReadIndex; } @@ -274,7 +273,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// @param _account The address of the juror. /// @param _courtID The ID of the court. /// @param _newStake The new stake. - /// @param _alreadyTransferred True if the tokens were already transferred from juror. Only relevant for delayed stakes. /// @return pnkDeposit The amount of PNK to be deposited. /// @return pnkWithdrawal The amount of PNK to be withdrawn. /// @return stakingResult The result of the staking operation. @@ -282,18 +280,18 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr address _account, uint96 _courtID, uint256 _newStake, - bool _alreadyTransferred + bool /*_alreadyTransferred*/ ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - (pnkDeposit, pnkWithdrawal, stakingResult) = _setStake(_account, _courtID, _newStake, _alreadyTransferred); + (pnkDeposit, pnkWithdrawal, stakingResult) = _setStake(_account, _courtID, _newStake, false); // The last parameter is unused. } /// @dev Sets the specified juror's stake in a court. - /// Note: no state changes should be made when returning `succeeded` = false, otherwise delayed stakes might break invariants. + /// Note: no state changes should be made when returning stakingResult != Successful, otherwise delayed stakes might break invariants. function _setStake( address _account, uint96 _courtID, uint256 _newStake, - bool _alreadyTransferred + bool /*_alreadyTransferred*/ ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { Juror storage juror = jurors[_account]; uint256 currentStake = stakeOf(_account, _courtID); @@ -307,33 +305,58 @@ 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 normal stakes or delayed 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 Called by KC at the end of setStake flow. + function updateState( + address _account, + uint96 _courtID, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal, + uint256 _newStake + ) external override onlyByCore { + Juror storage juror = jurors[_account]; + if (_pnkDeposit > 0) { + uint256 currentStake = stakeOf(_account, _courtID); + if (currentStake == 0) { + juror.courtIDs.push(_courtID); + } + // Increase juror's balance by deposited amount. + juror.stakedPnk += _pnkDeposit; } else { - 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 +373,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 { @@ -451,13 +385,23 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr emit StakeLocked(_account, _relativeAmount, true); } - function penalizeStake(address _account, uint256 _relativeAmount) external override onlyByCore { + function penalizeStake( + address _account, + uint256 _relativeAmount + ) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) { Juror storage juror = jurors[_account]; - if (juror.stakedPnk >= _relativeAmount) { + uint256 stakedPnk = juror.stakedPnk; + + if (stakedPnk >= _relativeAmount) { + availablePenalty = _relativeAmount; juror.stakedPnk -= _relativeAmount; } else { - juror.stakedPnk = 0; // stakedPnk might become lower after manual unstaking, but lockedPnk will always cover the difference. + availablePenalty = stakedPnk; + juror.stakedPnk = 0; } + + pnkBalance = juror.stakedPnk; + return (pnkBalance, availablePenalty); } /// @dev Unstakes the inactive juror from all courts. @@ -474,6 +418,26 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } } + /// @dev Gives back the locked PNKs in case the juror fully unstaked earlier. + /// Note that since locked and staked PNK are async it is possible for the juror to have positive staked PNK balance + /// while having 0 stake in courts and 0 locked tokens (eg. when the juror fully unstaked during dispute and later got his tokens unlocked). + /// In this case the juror can use this function to withdraw the leftover tokens. + /// Also note that if the juror has some leftover PNK while not fully unstaked he'll have to manually unstake from all courts to trigger this function. + /// @param _account The juror whose PNK to withdraw. + function withdrawLeftoverPNK(address _account) external override { + Juror storage juror = jurors[_account]; + // Can withdraw the leftover PNK if fully unstaked, has no tokens locked and has positive balance. + // This withdrawal can't be triggered by calling setStake() in KlerosCore because current stake is technically 0, thus it is done via separate function. + if (juror.stakedPnk > 0 && juror.courtIDs.length == 0 && juror.lockedPnk == 0) { + uint256 amount = juror.stakedPnk; + juror.stakedPnk = 0; + core.transferBySortitionModule(_account, amount); + emit LeftoverPNKWithdrawn(_account, amount); + } else { + revert("Not eligible for withdrawal."); + } + } + // ************************************* // // * Public Views * // // ************************************* // diff --git a/contracts/src/arbitration/SortitionModuleNeo.sol b/contracts/src/arbitration/SortitionModuleNeo.sol index 2e60307d2..712150037 100644 --- a/contracts/src/arbitration/SortitionModuleNeo.sol +++ b/contracts/src/arbitration/SortitionModuleNeo.sol @@ -88,13 +88,13 @@ contract SortitionModuleNeo is SortitionModuleBase { address _account, uint96 _courtID, uint256 _newStake, - bool _alreadyTransferred + bool /*_alreadyTransferred*/ ) internal override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { uint256 currentStake = stakeOf(_account, _courtID); bool stakeIncrease = _newStake > currentStake; uint256 stakeChange = stakeIncrease ? _newStake - currentStake : currentStake - _newStake; Juror storage juror = jurors[_account]; - if (stakeIncrease && !_alreadyTransferred) { + if (stakeIncrease) { if (juror.stakedPnk + stakeChange > maxStakePerJuror) { return (0, 0, StakingResult.CannotStakeMoreThanMaxStakePerJuror); } @@ -113,7 +113,7 @@ contract SortitionModuleNeo is SortitionModuleBase { _account, _courtID, _newStake, - _alreadyTransferred + false // This parameter is not used ); } } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index e3ed491eb..6707bd91b 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -610,7 +610,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. @@ -620,19 +620,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..a7c1499fd 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModule.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModule.sol @@ -27,7 +27,10 @@ interface ISortitionModule { function unlockStake(address _account, uint256 _relativeAmount) external; - function penalizeStake(address _account, uint256 _relativeAmount) external; + function penalizeStake( + address _account, + uint256 _relativeAmount + ) external returns (uint256 pnkBalance, uint256 availablePenalty); function notifyRandomNumber(uint256 _drawnNumber) external; @@ -45,4 +48,14 @@ interface ISortitionModule { function createDisputeHook(uint256 _disputeID, uint256 _roundID) external; function postDrawHook(uint256 _disputeID, uint256 _roundID) external; + + function withdrawLeftoverPNK(address _account) external; + + function updateState( + address _account, + uint96 _courtID, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal, + uint256 _newStake + ) external; } diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index b178c8b75..795870e00 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -235,7 +235,10 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, emit StakeLocked(_account, _relativeAmount, true); } - function penalizeStake(address _account, uint256 _relativeAmount) external override onlyByCore { + function penalizeStake( + address _account, + uint256 _relativeAmount + ) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) { Juror storage juror = jurors[_account]; if (juror.stakedPnk >= _relativeAmount) { juror.stakedPnk -= _relativeAmount; @@ -258,6 +261,8 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, } } + function withdrawLeftoverPNK(address _account) external override {} + // ************************************* // // * Public Views * // // ************************************* // @@ -312,6 +317,14 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, return jurors[_juror].stakedPnk > 0; } + function updateState( + address _account, + uint96 _courtID, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal, + uint256 _newStake + ) external {} + // ************************************* // // * Internal * // // ************************************* // diff --git a/contracts/src/libraries/Constants.sol b/contracts/src/libraries/Constants.sol index f393b4792..f2101461b 100644 --- a/contracts/src/libraries/Constants.sol +++ b/contracts/src/libraries/Constants.sol @@ -27,6 +27,7 @@ enum OnError { enum StakingResult { Successful, + Delayed, StakingTransferFailed, UnstakingTransferFailed, CannotStakeInMoreCourts, diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index 291b969d0..7d3e85953 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -1010,7 +1010,7 @@ contract KlerosCoreTest is Test { vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayedAlreadyTransferredDeposited(staker1, GENERAL_COURT, 1500); + emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1500); core.setStake(GENERAL_COURT, 1500); uint256 delayedStakeId = sortitionModule.delayedStakeWriteIndex(); @@ -1022,11 +1022,11 @@ contract KlerosCoreTest is Test { assertEq(account, staker1, "Wrong staker account"); assertEq(courtID, GENERAL_COURT, "Wrong court id"); assertEq(stake, 1500, "Wrong amount staked in court"); - assertEq(alreadyTransferred, true, "Should be flagged as transferred"); + assertEq(alreadyTransferred, false, "Should be flagged as transferred"); (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule .getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 1500, "Wrong amount total staked"); + assertEq(totalStaked, 1000, "Wrong amount total staked"); assertEq(totalLocked, 0, "Wrong amount locked"); assertEq(stakedInCourt, 1000, "Amount staked in court should not change until delayed stake is executed"); assertEq(nbCourts, 1, "Wrong number of courts"); @@ -1036,9 +1036,8 @@ contract KlerosCoreTest is Test { assertEq(courts[0], GENERAL_COURT, "Wrong court id"); assertEq(sortitionModule.isJurorStaked(staker1), true, "Juror should be staked"); - assertEq(pinakion.balanceOf(address(core)), 1500, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of staker1"); - assertEq(pinakion.allowance(staker1, address(core)), 999999999999998500, "Wrong allowance amount"); + assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); } function test_setStake_decreaseDrawingPhase() public { @@ -1057,7 +1056,7 @@ contract KlerosCoreTest is Test { vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayedNotTransferred(staker1, GENERAL_COURT, 1800); + emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1800); core.setStake(GENERAL_COURT, 1800); (uint256 totalStaked, , uint256 stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); @@ -1092,12 +1091,12 @@ contract KlerosCoreTest is Test { assertEq(pinakion.balanceOf(address(core)), 10000, "Wrong token balance of the core"); assertEq(pinakion.balanceOf(staker1), 999999999999990000, "Wrong token balance of staker1"); - // Unstake to check that locked tokens will remain + // Unstake to check that locked tokens won't be withdrawn vm.prank(staker1); core.setStake(GENERAL_COURT, 0); (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 0, "Wrong amount total staked"); + assertEq(totalStaked, 3000, "Wrong amount total staked"); assertEq(totalLocked, 3000, "Wrong amount locked"); assertEq(stakedInCourt, 0, "Wrong amount staked in court"); assertEq(nbCourts, 0, "Wrong amount staked in court"); @@ -1105,19 +1104,18 @@ contract KlerosCoreTest is Test { assertEq(pinakion.balanceOf(address(core)), 3000, "Wrong token balance of the core"); assertEq(pinakion.balanceOf(staker1), 999999999999997000, "Wrong token balance of staker1"); - // Stake again to see that locked tokens will count when increasing the stake. We check that the court won't take the full stake - // but only the remaining part. + // Stake again to check the behaviour. vm.prank(staker1); core.setStake(GENERAL_COURT, 5000); (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 5000, "Wrong amount total staked"); + assertEq(totalStaked, 8000, "Wrong amount total staked"); // 5000 were added to the previous 3000. assertEq(totalLocked, 3000, "Wrong amount locked"); assertEq(stakedInCourt, 5000, "Wrong amount staked in court"); assertEq(nbCourts, 1, "Wrong amount staked in court"); - assertEq(pinakion.balanceOf(address(core)), 5000, "Locked tokens should stay in the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999995000, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(address(core)), 8000, "Wrong amount of tokens in Core"); + assertEq(pinakion.balanceOf(staker1), 999999999999992000, "Wrong token balance of staker1"); } function test_executeDelayedStakes() public { @@ -1141,18 +1139,26 @@ contract KlerosCoreTest is Test { vm.expectRevert(bytes("Should be in Staking phase.")); sortitionModule.executeDelayedStakes(5); + // Create delayed stake vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1500); core.setStake(GENERAL_COURT, 1500); - assertEq(pinakion.balanceOf(address(core)), 11500, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(address(core)), 10000, "Wrong token balance of the core"); // Balance should not increase because the stake was delayed + assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); + + // Create delayed stake for another staker vm.prank(staker2); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeDelayed(staker2, GENERAL_COURT, 0); core.setStake(GENERAL_COURT, 0); assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); // Balance should not change since wrong phase + // Create another delayed stake for staker1 on top of it to check the execution vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayedAlreadyTransferredWithdrawn(staker1, GENERAL_COURT, 1500); + emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1800); core.setStake(GENERAL_COURT, 1800); assertEq(sortitionModule.delayedStakeWriteIndex(), 3, "Wrong delayedStakeWriteIndex"); @@ -1160,42 +1166,47 @@ contract KlerosCoreTest is Test { (address account, uint96 courtID, uint256 stake, bool alreadyTransferred) = sortitionModule.delayedStakes(1); - // First delayed stake should be nullified - assertEq(account, address(0), "Wrong staker account after delayed stake deletion"); - assertEq(courtID, 0, "Court id should be nullified"); - assertEq(stake, 0, "No amount to stake"); + // Check each delayed stake + assertEq(account, staker1, "Wrong staker account for the first delayed stake"); + assertEq(courtID, GENERAL_COURT, "Wrong court ID"); + assertEq(stake, 1500, "Wrong staking amount"); assertEq(alreadyTransferred, false, "Should be false"); (account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(2); assertEq(account, staker2, "Wrong staker2 account"); assertEq(courtID, GENERAL_COURT, "Wrong court id for staker2"); assertEq(stake, 0, "Wrong amount for delayed stake of staker2"); - assertEq(alreadyTransferred, false, "Should be false for staker2"); + assertEq(alreadyTransferred, false, "Should be false"); (account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(3); assertEq(account, staker1, "Wrong staker1 account"); assertEq(courtID, GENERAL_COURT, "Wrong court id for staker1"); assertEq(stake, 1800, "Wrong amount for delayed stake of staker1"); - assertEq(alreadyTransferred, true, "Should be true for staker1"); + assertEq(alreadyTransferred, false, "Should be false"); - assertEq(pinakion.balanceOf(address(core)), 11800, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999998200, "Wrong token balance of staker1"); + // So far the only amount transferred was 10000 by staker2. Staker 1 has two delayed stakes, for 1500 and 1800 pnk. + assertEq(pinakion.balanceOf(address(core)), 10000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule - .getJurorBalance(staker1, GENERAL_COURT); // Only check the first staker since he has consecutive delayed stakes - assertEq(totalStaked, 1800, "Wrong amount total staked"); + .getJurorBalance(staker1, GENERAL_COURT); // Only check the first staker to check how consecutive delayed stakes are handled. + // Balances shouldn't be updated yet. + assertEq(totalStaked, 0, "Wrong amount total staked"); assertEq(totalLocked, 0, "Wrong amount locked"); assertEq(stakedInCourt, 0, "Wrong amount staked in court"); - assertEq(nbCourts, 1, "Wrong amount staked in court"); + assertEq(nbCourts, 0, "Wrong number of courts"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Staking. Delayed stakes can be executed now vm.prank(address(core)); pinakion.transfer(governor, 10000); // Dispose of the tokens of 2nd staker to make the execution fail for the 2nd delayed stake - assertEq(pinakion.balanceOf(address(core)), 1800, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); + // 2 events should be emitted but the 2nd stake supersedes the first one in the end. + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1500, 1500); vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1800, 1800); sortitionModule.executeDelayedStakes(20); // Deliberately ask for more iterations than needed @@ -1225,54 +1236,6 @@ contract KlerosCoreTest is Test { assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); } - function test_deleteDelayedStake() public { - // Check that the delayed stake gets deleted without execution if the juror changed his stake in staking phase before its execution. - vm.prank(staker1); - core.setStake(GENERAL_COURT, 1000); - - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); - sortitionModule.passPhase(); // Drawing phase - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 1500); // Create delayed stake - - (uint256 totalStaked, , uint256 stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 1500, "Wrong amount total staked"); - assertEq(stakedInCourt, 1000, "Wrong amount staked in court"); - assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of the staker1"); - assertEq(pinakion.balanceOf(address(core)), 1500, "Wrong token balance of the core"); - - (address account, uint96 courtID, uint256 stake, bool alreadyTransferred) = sortitionModule.delayedStakes(1); - assertEq(account, staker1, "Wrong account"); - assertEq(courtID, GENERAL_COURT, "Wrong court id"); - assertEq(stake, 1500, "Wrong amount for delayed stake"); - assertEq(alreadyTransferred, true, "Should be true"); - - vm.warp(block.timestamp + maxDrawingTime); - sortitionModule.passPhase(); // Staking phase - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 1700); // Set stake 2nd time, this time in staking phase to see that the delayed stake will be nullified. - - (totalStaked, , stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 1700, "Wrong amount total staked"); - assertEq(stakedInCourt, 1700, "Wrong amount staked in court"); - assertEq(pinakion.balanceOf(staker1), 999999999999998300, "Wrong token balance of the staker1"); - assertEq(pinakion.balanceOf(address(core)), 1700, "Wrong token balance of the core"); - - sortitionModule.executeDelayedStakes(1); - (account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(1); - // Check that delayed stake is deleted - assertEq(account, address(0), "Wrong staker account after delayed stake deletion"); - assertEq(courtID, 0, "Court id should be nullified"); - assertEq(stake, 0, "No amount to stake"); - assertEq(alreadyTransferred, false, "Should be false"); - } - function test_setStakeBySortitionModule() public { // Note that functionality of this function was checked during delayed stakes execution vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector); @@ -1463,43 +1426,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"); @@ -2474,6 +2411,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); @@ -2496,26 +2436,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); @@ -2543,27 +2542,48 @@ contract KlerosCoreTest is Test { core.passPeriod(disputeID); // Execution vm.prank(staker1); - core.setStake(GENERAL_COURT, 0); + core.setStake(GENERAL_COURT, 0); // Set stake to 0 to check if it will be withdrawn later. (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule .getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 0, "Should be unstaked"); + assertEq(totalStaked, 1000, "Wrong amount staked"); assertEq(totalLocked, 3000, "Wrong amount locked"); assertEq(stakedInCourt, 0, "Should be unstaked"); assertEq(nbCourts, 0, "Should be 0 courts"); - assertEq(pinakion.balanceOf(address(core)), 3000, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999997000, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); + + vm.expectRevert(bytes("Not eligible for withdrawal.")); + sortitionModule.withdrawLeftoverPNK(staker1); core.execute(disputeID, 0, 6); + (totalStaked, totalLocked, , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 1000, "Wrong amount staked"); + assertEq(totalLocked, 0, "Should be fully unlocked"); + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.pnkPenalties, 0, "Wrong pnkPenalties"); assertEq(round.sumFeeRewardPaid, 0.09 ether, "Wrong sumFeeRewardPaid"); assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); // No penalty so no rewards in pnk - assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); + // Execute() shouldn't withdraw the tokens. + assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); + + vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector); + vm.prank(governor); + core.transferBySortitionModule(staker1, 1000); + + sortitionModule.withdrawLeftoverPNK(staker1); + + (totalStaked, , , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 0, "Should be unstaked fully"); + + // Check that everything is withdrawn now + assertEq(pinakion.balanceOf(address(core)), 0, "Core balance should be empty"); + assertEq(pinakion.balanceOf(staker1), 1 ether, "All PNK should be withdrawn"); } function test_execute_feeToken() public { diff --git a/subgraph/core-neo/abi-migrations/SortitionModuleNeo.json b/subgraph/core-neo/abi-migrations/SortitionModuleNeo.json index b65294575..f7a756723 100644 --- a/subgraph/core-neo/abi-migrations/SortitionModuleNeo.json +++ b/subgraph/core-neo/abi-migrations/SortitionModuleNeo.json @@ -60,34 +60,15 @@ "name": "Initialized", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "enum ISortitionModule.Phase", - "name": "_phase", - "type": "uint8" - } - ], - "name": "NewPhase", - "type": "event" - }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", - "name": "_address", + "name": "_account", "type": "address" }, - { - "indexed": false, - "internalType": "uint256", - "name": "_courtID", - "type": "uint256" - }, { "indexed": false, "internalType": "uint256", @@ -95,32 +76,20 @@ "type": "uint256" } ], - "name": "StakeDelayedAlreadyTransferredDeposited", + "name": "LeftoverPNKWithdrawn", "type": "event" }, { "anonymous": false, "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_address", - "type": "address" - }, { "indexed": false, - "internalType": "uint256", - "name": "_courtID", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_amount", - "type": "uint256" + "internalType": "enum ISortitionModule.Phase", + "name": "_phase", + "type": "uint8" } ], - "name": "StakeDelayedAlreadyTransferred", + "name": "NewPhase", "type": "event" }, { @@ -145,32 +114,7 @@ "type": "uint256" } ], - "name": "StakeDelayedAlreadyTransferredWithdrawn", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_address", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_courtID", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - } - ], - "name": "StakeDelayedNotTransferred", + "name": "StakeDelayed", "type": "event" }, { @@ -449,11 +393,6 @@ "internalType": "uint256", "name": "stake", "type": "uint256" - }, - { - "internalType": "bool", - "name": "alreadyTransferred", - "type": "bool" } ], "stateMutability": "view", @@ -696,30 +635,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "jurorAccount", - "type": "address" - }, - { - "internalType": "uint96", - "name": "courtId", - "type": "uint96" - } - ], - "name": "latestDelayedStakeIndex", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -824,7 +739,18 @@ } ], "name": "penalizeStake", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "pnkBalance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "availablePenalty", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, @@ -956,7 +882,7 @@ }, { "internalType": "bool", - "name": "_alreadyTransferred", + "name": "", "type": "bool" } ], @@ -972,6 +898,11 @@ "name": "pnkWithdrawal", "type": "uint256" }, + { + "internalType": "uint256", + "name": "oldStake", + "type": "uint256" + }, { "internalType": "enum StakingResult", "name": "stakingResult", @@ -1090,6 +1021,19 @@ ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_account", + "type": "address" + } + ], + "name": "withdrawLeftoverPNK", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] } diff --git a/subgraph/core-neo/subgraph.yaml b/subgraph/core-neo/subgraph.yaml index 69e4b7d01..10a37fb5b 100644 --- a/subgraph/core-neo/subgraph.yaml +++ b/subgraph/core-neo/subgraph.yaml @@ -158,15 +158,8 @@ dataSources: # FIX: temporarily point to abi with event addition file: ./abi-migrations/SortitionModuleNeo.json eventHandlers: - - event: StakeDelayedAlreadyTransferredDeposited(indexed address,uint256,uint256) - handler: handleStakeDelayedAlreadyTransferredDeposited - # FIX: temporarily indexing old event name - - event: StakeDelayedAlreadyTransferred(indexed address,uint256,uint256) - handler: handleStakeDelayedAlreadyTransferred - - event: StakeDelayedAlreadyTransferredWithdrawn(indexed address,indexed uint96,uint256) - handler: handleStakeDelayedAlreadyTransferredWithdrawn - - event: StakeDelayedNotTransferred(indexed address,uint256,uint256) - handler: handleStakeDelayedNotTransferred + - event: StakeDelayed(indexed address,indexed uint96,uint256) + handler: handleStakeDelayed - event: StakeLocked(indexed address,uint256,bool) handler: handleStakeLocked - event: StakeSet(indexed address,uint256,uint256,uint256) diff --git a/subgraph/core/abi-migrations/SortitionModule.json b/subgraph/core/abi-migrations/SortitionModule.json index d9ba2ca9e..859f4ae9f 100644 --- a/subgraph/core/abi-migrations/SortitionModule.json +++ b/subgraph/core/abi-migrations/SortitionModule.json @@ -1,12 +1,9 @@ { "abi": [ { - "stateMutability": "payable", - "type": "fallback" - }, - { - "stateMutability": "payable", - "type": "receive" + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" }, { "inputs": [], @@ -63,34 +60,15 @@ "name": "Initialized", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "enum ISortitionModule.Phase", - "name": "_phase", - "type": "uint8" - } - ], - "name": "NewPhase", - "type": "event" - }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", - "name": "_address", + "name": "_account", "type": "address" }, - { - "indexed": false, - "internalType": "uint256", - "name": "_courtID", - "type": "uint256" - }, { "indexed": false, "internalType": "uint256", @@ -98,32 +76,20 @@ "type": "uint256" } ], - "name": "StakeDelayedAlreadyTransferredDeposited", + "name": "LeftoverPNKWithdrawn", "type": "event" }, { "anonymous": false, "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_address", - "type": "address" - }, { "indexed": false, - "internalType": "uint256", - "name": "_courtID", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_amount", - "type": "uint256" + "internalType": "enum ISortitionModule.Phase", + "name": "_phase", + "type": "uint8" } ], - "name": "StakeDelayedAlreadyTransferred", + "name": "NewPhase", "type": "event" }, { @@ -148,32 +114,7 @@ "type": "uint256" } ], - "name": "StakeDelayedAlreadyTransferredWithdrawn", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_address", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_courtID", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - } - ], - "name": "StakeDelayedNotTransferred", + "name": "StakeDelayed", "type": "event" }, { @@ -426,11 +367,6 @@ "internalType": "uint256", "name": "stake", "type": "uint256" - }, - { - "internalType": "bool", - "name": "alreadyTransferred", - "type": "bool" } ], "stateMutability": "view", @@ -663,30 +599,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "jurorAccount", - "type": "address" - }, - { - "internalType": "uint96", - "name": "courtId", - "type": "uint96" - } - ], - "name": "latestDelayedStakeIndex", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -765,7 +677,18 @@ } ], "name": "penalizeStake", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "pnkBalance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "availablePenalty", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, @@ -897,7 +820,7 @@ }, { "internalType": "bool", - "name": "_alreadyTransferred", + "name": "", "type": "bool" } ], @@ -913,6 +836,11 @@ "name": "pnkWithdrawal", "type": "uint256" }, + { + "internalType": "uint256", + "name": "oldStake", + "type": "uint256" + }, { "internalType": "enum StakingResult", "name": "stakingResult", @@ -1023,17 +951,14 @@ "inputs": [ { "internalType": "address", - "name": "_implementation", + "name": "_account", "type": "address" - }, - { - "internalType": "bytes", - "name": "_data", - "type": "bytes" } ], + "name": "withdrawLeftoverPNK", + "outputs": [], "stateMutability": "nonpayable", - "type": "constructor" + "type": "function" } ] } diff --git a/subgraph/core/src/SortitionModule.ts b/subgraph/core/src/SortitionModule.ts index 4d4f8c895..672a994ba 100644 --- a/subgraph/core/src/SortitionModule.ts +++ b/subgraph/core/src/SortitionModule.ts @@ -1,31 +1,10 @@ -import { - SortitionModule, - StakeDelayedAlreadyTransferred, - StakeDelayedAlreadyTransferredDeposited, - StakeDelayedAlreadyTransferredWithdrawn, - StakeDelayedNotTransferred, - StakeLocked, - StakeSet, -} from "../generated/SortitionModule/SortitionModule"; +import { SortitionModule, StakeDelayed, StakeLocked, StakeSet } from "../generated/SortitionModule/SortitionModule"; import { updateJurorDelayedStake, updateJurorStake } from "./entities/JurorTokensPerCourt"; import { ensureUser } from "./entities/User"; import { ZERO } from "./utils"; -// FIX: temporarily adding this handler for old event name "StakeDelayedAlreadyTransferred", delete when deploying new fresh-address contract. -export function handleStakeDelayedAlreadyTransferred(event: StakeDelayedAlreadyTransferred): void { - updateJurorDelayedStake(event.params._address.toHexString(), event.params._courtID.toString(), event.params._amount); -} - -export function handleStakeDelayedAlreadyTransferredDeposited(event: StakeDelayedAlreadyTransferredDeposited): void { - updateJurorDelayedStake(event.params._address.toHexString(), event.params._courtID.toString(), event.params._amount); -} - -export function handleStakeDelayedAlreadyTransferredWithdrawn(event: StakeDelayedAlreadyTransferredWithdrawn): void { - updateJurorDelayedStake(event.params._address.toHexString(), event.params._courtID.toString(), event.params._amount); -} - -export function handleStakeDelayedNotTransferred(event: StakeDelayedNotTransferred): void { +export function handleStakeDelayed(event: StakeDelayed): void { updateJurorDelayedStake(event.params._address.toHexString(), event.params._courtID.toString(), event.params._amount); } diff --git a/subgraph/core/subgraph.yaml b/subgraph/core/subgraph.yaml index d3aff9873..e8c0cd3f6 100644 --- a/subgraph/core/subgraph.yaml +++ b/subgraph/core/subgraph.yaml @@ -159,15 +159,8 @@ dataSources: # FIX: temporarily point to abi with event addition file: ./abi-migrations/SortitionModule.json eventHandlers: - - event: StakeDelayedAlreadyTransferredDeposited(indexed address,uint256,uint256) - handler: handleStakeDelayedAlreadyTransferredDeposited - # FIX: temporarily indexing old event name - - event: StakeDelayedAlreadyTransferred(indexed address,uint256,uint256) - handler: handleStakeDelayedAlreadyTransferred - - event: StakeDelayedAlreadyTransferredWithdrawn(indexed address,indexed uint96,uint256) - handler: handleStakeDelayedAlreadyTransferredWithdrawn - - event: StakeDelayedNotTransferred(indexed address,uint256,uint256) - handler: handleStakeDelayedNotTransferred + - event: StakeDelayed(indexed address,indexed uint96,uint256) + handler: handleStakeDelayed - event: StakeLocked(indexed address,uint256,bool) handler: handleStakeLocked - event: StakeSet(indexed address,uint256,uint256,uint256) diff --git a/subgraph/temp-older-events-addition.txt b/subgraph/temp-older-events-addition.txt index abf0ca62f..f049dcc70 100644 --- a/subgraph/temp-older-events-addition.txt +++ b/subgraph/temp-older-events-addition.txt @@ -92,7 +92,7 @@ -// Goes in SortitionModule.json: rename of StakeDelayedAlreadyTransferred => StakeDelayedAlreadyTransferredDeposited +// NOT NEEDED ANYMORE: Goes in SortitionModule.json: rename of StakeDelayedAlreadyTransferred => StakeDelayedAlreadyTransferredDeposited { "anonymous": false, "inputs": [