diff --git a/contracts/APRCalculator/APRCalculator.sol b/contracts/APRCalculator/APRCalculator.sol index 0591e240..d8094d09 100644 --- a/contracts/APRCalculator/APRCalculator.sol +++ b/contracts/APRCalculator/APRCalculator.sol @@ -99,58 +99,60 @@ contract APRCalculator is IAPRCalculator, MacroFactor, RSIndex { * @notice Initializes vesting bonus for each week. */ function _initializeVestingBonus() private { - vestingBonus[0] = 6; - vestingBonus[1] = 16; - vestingBonus[2] = 30; - vestingBonus[3] = 46; - vestingBonus[4] = 65; - vestingBonus[5] = 85; - vestingBonus[6] = 108; - vestingBonus[7] = 131; - vestingBonus[8] = 157; - vestingBonus[9] = 184; - vestingBonus[10] = 212; - vestingBonus[11] = 241; - vestingBonus[12] = 272; - vestingBonus[13] = 304; - vestingBonus[14] = 338; - vestingBonus[15] = 372; - vestingBonus[16] = 407; - vestingBonus[17] = 444; - vestingBonus[18] = 481; - vestingBonus[19] = 520; - vestingBonus[20] = 559; - vestingBonus[21] = 599; - vestingBonus[22] = 641; - vestingBonus[23] = 683; - vestingBonus[24] = 726; - vestingBonus[25] = 770; - vestingBonus[26] = 815; - vestingBonus[27] = 861; - vestingBonus[28] = 907; - vestingBonus[29] = 955; - vestingBonus[30] = 1003; - vestingBonus[31] = 1052; - vestingBonus[32] = 1101; - vestingBonus[33] = 1152; - vestingBonus[34] = 1203; - vestingBonus[35] = 1255; - vestingBonus[36] = 1307; - vestingBonus[37] = 1361; - vestingBonus[38] = 1415; - vestingBonus[39] = 1470; - vestingBonus[40] = 1525; - vestingBonus[41] = 1581; - vestingBonus[42] = 1638; - vestingBonus[43] = 1696; - vestingBonus[44] = 1754; - vestingBonus[45] = 1812; - vestingBonus[46] = 1872; - vestingBonus[47] = 1932; - vestingBonus[48] = 1993; - vestingBonus[49] = 2054; - vestingBonus[50] = 2116; - vestingBonus[51] = 2178; + vestingBonus = [ + 6, + 16, + 30, + 46, + 65, + 85, + 108, + 131, + 157, + 184, + 212, + 241, + 272, + 304, + 338, + 372, + 407, + 444, + 481, + 520, + 559, + 599, + 641, + 683, + 726, + 770, + 815, + 861, + 907, + 955, + 1003, + 1052, + 1101, + 1152, + 1203, + 1255, + 1307, + 1361, + 1415, + 1470, + 1525, + 1581, + 1638, + 1696, + 1754, + 1812, + 1872, + 1932, + 1993, + 2054, + 2116, + 2178 + ]; } // slither-disable-next-line unused-state,naming-convention diff --git a/contracts/HydraChain/modules/DaoIncentive/DaoIncentive.sol b/contracts/HydraChain/modules/DaoIncentive/DaoIncentive.sol index a3826c31..6e5bb64c 100644 --- a/contracts/HydraChain/modules/DaoIncentive/DaoIncentive.sol +++ b/contracts/HydraChain/modules/DaoIncentive/DaoIncentive.sol @@ -39,8 +39,9 @@ abstract contract DaoIncentive is IDaoIncentive, System, Initializable, RewardWa * @inheritdoc IDaoIncentive */ function distributeDAOIncentive() external onlySystemCall { - uint256 reward = (((hydraStakingContract.totalBalance() * 200) / 10000) * - (block.timestamp - lastDistribution)) / 365 days; + uint256 reward = ( + ((hydraStakingContract.totalBalance() * 200 * (block.timestamp - lastDistribution)) / (10000 * 365 days)) + ); lastDistribution = block.timestamp; vaultDistribution += reward; @@ -51,15 +52,15 @@ abstract contract DaoIncentive is IDaoIncentive, System, Initializable, RewardWa * @inheritdoc IDaoIncentive */ function claimVaultFunds() external { - uint256 reward = vaultDistribution; - if (reward == 0) { + uint256 incentive = vaultDistribution; + if (incentive == 0) { revert NoVaultFundsToClaim(); } vaultDistribution = 0; - rewardWalletContract.distributeReward(daoIncentiveVaultContract, reward); + rewardWalletContract.distributeReward(daoIncentiveVaultContract, incentive); - emit VaultFunded(reward); + emit VaultFunded(incentive); } // slither-disable-next-line unused-state,naming-convention diff --git a/contracts/HydraChain/modules/Inspector/Inspector.sol b/contracts/HydraChain/modules/Inspector/Inspector.sol index 0a644f59..fcc6a10d 100644 --- a/contracts/HydraChain/modules/Inspector/Inspector.sol +++ b/contracts/HydraChain/modules/Inspector/Inspector.sol @@ -12,9 +12,13 @@ abstract contract Inspector is IInspector, ValidatorManager { uint256 public validatorPenalty; /// @notice The reward for the person who reports a validator that have to be banned uint256 public reporterReward; - /// @notice Validator inactiveness (in blocks) threshold that needs to be passed to initiate ban for a validator + /// @notice Threshold for validator inactiveness (in blocks). + /// A ban can be initiated for a validator if this threshold is reached or exceeded. + /// @dev must be always bigger than the epoch length (better bigger than at least 4 epochs), + /// otherwise all validators can be banned uint256 public initiateBanThreshold; - /// @notice Validator inactiveness (in seconds) threshold that needs to be passed to ban a validator + /// @notice Threshold for validator inactiveness (in seconds). A validator can be banned + /// if it remains in the ban-initiated state for a duration equal to or exceeding this threshold. uint256 public banThreshold; /// @notice Mapping of the validators that bans has been initiated for (validator => timestamp) mapping(address => uint256) public bansInitiated; diff --git a/contracts/HydraChain/modules/ValidatorManager/ValidatorManager.sol b/contracts/HydraChain/modules/ValidatorManager/ValidatorManager.sol index bd3d3ed7..8bc60f6f 100644 --- a/contracts/HydraChain/modules/ValidatorManager/ValidatorManager.sol +++ b/contracts/HydraChain/modules/ValidatorManager/ValidatorManager.sol @@ -36,6 +36,7 @@ abstract contract ValidatorManager is mapping(address => Validator) public validators; /** * @notice Mapping that keeps the last time when a validator has participated in the consensus + * @dev Updated on epoch-ending blocks only * @dev Keep in mind that the validator will initially be set active when stake, * but it will be able to participate in the next epoch. So, the validator will have * less blocks to participate before getting eligible for ban. @@ -72,22 +73,6 @@ abstract contract ValidatorManager is } } - // _______________ Modifiers _______________ - - modifier onlyActiveValidator(address validator) { - if (validators[validator].status != ValidatorStatus.Active) revert Unauthorized("INACTIVE_VALIDATOR"); - _; - } - - /// @notice Modifier to check if the validator is registered or active - modifier onlyValidator(address validator) { - if ( - validators[validator].status != ValidatorStatus.Registered && - validators[validator].status != ValidatorStatus.Active - ) revert Unauthorized("INVALID_VALIDATOR"); - _; - } - // _______________ External functions _______________ /** diff --git a/contracts/HydraDelegation/Delegation.sol b/contracts/HydraDelegation/Delegation.sol index 5e9f2a83..388a4b5e 100644 --- a/contracts/HydraDelegation/Delegation.sol +++ b/contracts/HydraDelegation/Delegation.sol @@ -216,12 +216,12 @@ contract Delegation is } /** - * @notice Base delegation funds to a staker + * @notice Core logic for delegating funds to a staker * @param staker Address of the validator * @param delegator Address of the delegator * @param amount Amount to delegate */ - function _baseDelegate(address staker, address delegator, uint256 amount) internal virtual { + function _baseDelegate(address staker, address delegator, uint256 amount) internal { if (amount == 0) revert DelegateRequirement({src: "delegate", msg: "DELEGATING_AMOUNT_ZERO"}); DelegationPool storage delegation = delegationPools[staker]; uint256 delegatedAmount = delegation.balanceOf(delegator); @@ -253,6 +253,7 @@ contract Delegation is /** * @notice Undelegates funds from a staker + * @dev overridden in child contracts to extend core undelegate behaviour * @param staker Address of the validator * @param delegator Address of the delegator * @param amount Amount to delegate @@ -262,12 +263,12 @@ contract Delegation is } /** - * @notice Base undelegating funds from a staker + * @notice Core logic for undelegating funds from a staker * @param staker Address of the validator * @param delegator Address of the delegator * @param amount Amount to delegate */ - function _baseUndelegate(address staker, address delegator, uint256 amount) internal virtual { + function _baseUndelegate(address staker, address delegator, uint256 amount) internal { DelegationPool storage delegation = delegationPools[staker]; uint256 delegatedAmount = delegation.balanceOf(delegator); if (amount > delegatedAmount) revert DelegateRequirement({src: "undelegate", msg: "INSUFFICIENT_BALANCE"}); @@ -290,6 +291,7 @@ contract Delegation is /** * @notice Withdraws funds from stakers pool + * @dev overridden in child contracts to extend core withdraw behaviour * @param delegation Delegation pool * @param delegator Address of the delegator * @param amount Amount to withdraw diff --git a/contracts/HydraDelegation/HydraDelegation.sol b/contracts/HydraDelegation/HydraDelegation.sol index 75726e6b..5059b098 100644 --- a/contracts/HydraDelegation/HydraDelegation.sol +++ b/contracts/HydraDelegation/HydraDelegation.sol @@ -39,7 +39,6 @@ contract HydraDelegation is IHydraDelegation, System, Delegation, LiquidDelegati __Liquid_init(liquidToken); __Vesting_init_unchained(); __VestingManagerFactoryConnector_init(vestingManagerFactoryAddr); - __VestedDelegation_init_unchained(); } // _______________ External functions _______________ @@ -60,7 +59,7 @@ contract HydraDelegation is IHydraDelegation, System, Delegation, LiquidDelegati address staker, address delegator, uint256 amount - ) internal virtual override(Delegation, LiquidDelegation, VestedDelegation) { + ) internal virtual override(Delegation, LiquidDelegation) { super._delegate(staker, delegator, amount); } @@ -71,7 +70,7 @@ contract HydraDelegation is IHydraDelegation, System, Delegation, LiquidDelegati address staker, address delegator, uint256 amount - ) internal virtual override(Delegation, LiquidDelegation, VestedDelegation) { + ) internal virtual override(Delegation, LiquidDelegation) { super._undelegate(staker, delegator, amount); } @@ -83,7 +82,18 @@ contract HydraDelegation is IHydraDelegation, System, Delegation, LiquidDelegati */ function _distributeTokens(address staker, address account, uint256 amount) internal virtual override { VestingPosition memory position = vestedDelegationPositions[staker][msg.sender]; + // This check works because if position has already been opened, the restrictions on delegateWithVesting() + // will prevent entering this check again if (_isOpeningPosition(position)) { + uint256 previousDelegation = delegationOf(staker, account) - amount; + if (previousDelegation != 0) { + // We want all previously distributed tokens to be collected, + // because the new position vesting period can be different from the previous one + // meaning the tokens to distribute amount will be different + _collectTokens(staker, previousDelegation); + amount += previousDelegation; + } + uint256 debt = _calculatePositionDebt(amount, position.duration); liquidityDebts[account] -= debt.toInt256Safe(); // Add negative debt amount -= debt; diff --git a/contracts/HydraDelegation/modules/DelegationPoolLib/DelegationPoolLib.sol b/contracts/HydraDelegation/modules/DelegationPoolLib/DelegationPoolLib.sol index 62103f8f..5d226669 100644 --- a/contracts/HydraDelegation/modules/DelegationPoolLib/DelegationPoolLib.sol +++ b/contracts/HydraDelegation/modules/DelegationPoolLib/DelegationPoolLib.sol @@ -245,7 +245,6 @@ library DelegationPoolLib { * @param currentEpochNum the current epoch number * @return bool whether the balance change is made */ - // TODO: Check if the commitEpoch is the last transaction in the epoch, otherwise bug may occur function isBalanceChangeMade( DelegationPool storage pool, address delegator, @@ -277,7 +276,8 @@ library DelegationPoolLib { // _______________ Private functions _______________ /** - * @notice Saves the RPS for the given staker for the epoch + * @notice Saves the RPS for the given staker's delegation pool + * @dev must be called when new reward is distributed (at the end of every epoch in our case) * @param pool the DelegationPool to save the RPS for * @param rewardPerShare Amount of tokens to be withdrawn * @param epochNumber Epoch number diff --git a/contracts/HydraDelegation/modules/VestedDelegation/VestedDelegation.sol b/contracts/HydraDelegation/modules/VestedDelegation/VestedDelegation.sol index 641e437a..9ad0be3b 100644 --- a/contracts/HydraDelegation/modules/VestedDelegation/VestedDelegation.sol +++ b/contracts/HydraDelegation/modules/VestedDelegation/VestedDelegation.sol @@ -16,12 +16,6 @@ abstract contract VestedDelegation is IVestedDelegation, Vesting, Delegation, Ve using DelegationPoolLib for DelegationPool; using VestedPositionLib for VestingPosition; - /** - * @notice The threshold for the maximum number of allowed balance changes - * @dev We are using this to restrict unlimited changes of the balance (delegationPoolParamsHistory) - */ - uint256 public balanceChangeThreshold; - /** * @notice The vesting positions for every delegator * @dev Staker => Delegator => VestingPosition @@ -52,12 +46,6 @@ abstract contract VestedDelegation is IVestedDelegation, Vesting, Delegation, Ve ); __Vesting_init(governance, aprCalculatorAddr); __VestingManagerFactoryConnector_init(vestingManagerFactoryAddr); - __VestedDelegation_init_unchained(); - } - - // solhint-disable-next-line func-name-mixedcase - function __VestedDelegation_init_unchained() internal onlyInitializing { - balanceChangeThreshold = 32; } // _______________ Modifiers _______________ @@ -202,7 +190,6 @@ abstract contract VestedDelegation is IVestedDelegation, Vesting, Delegation, Ve delegation.cleanDelegatorHistoricalData(msg.sender); uint256 duration = durationWeeks * 1 weeks; - // TODO: calculate end of period instead of write in the cold storage. It is cheaper vestedDelegationPositions[staker][msg.sender] = VestingPosition({ duration: duration, start: block.timestamp, @@ -223,7 +210,7 @@ abstract contract VestedDelegation is IVestedDelegation, Vesting, Delegation, Ve */ function swapVestedPositionStaker(address oldStaker, address newStaker) external onlyManager { VestingPosition memory oldPosition = vestedDelegationPositions[oldStaker][msg.sender]; - // ensure that the old position is active in order to continue the swap + // ensure that the old position is active if (!oldPosition.isActive()) { revert DelegateRequirement({src: "vesting", msg: "OLD_POSITION_INACTIVE"}); } @@ -341,7 +328,6 @@ abstract contract VestedDelegation is IVestedDelegation, Vesting, Delegation, Ve // _______________ Public functions _______________ - // TODO: Check if the commitEpoch is the last transaction in the epoch, otherwise bug may occur /** * @inheritdoc IVestedDelegation */ @@ -388,13 +374,6 @@ abstract contract VestedDelegation is IVestedDelegation, Vesting, Delegation, Ve // _______________ Internal functions _______________ - /** - * @inheritdoc Delegation - */ - function _delegate(address staker, address delegator, uint256 amount) internal virtual override { - super._delegate(staker, delegator, amount); - } - /** * @inheritdoc Delegation */ @@ -404,9 +383,9 @@ abstract contract VestedDelegation is IVestedDelegation, Vesting, Delegation, Ve address delegator, uint256 amount ) internal virtual override { - // If it is a vested delegation, withdraw by keeping the change in the delegation pool params + // If it is a vested delegation, deposit by keeping the change in the delegation pool params // so vested rewards claiming is possible - if (vestedDelegationPositions[staker][delegator].isInVestingCycle()) { + if (vestedDelegationPositions[staker][delegator].isActive()) { return delegation.deposit(delegator, amount, hydraChainContract.getCurrentEpochId()); } @@ -424,20 +403,13 @@ abstract contract VestedDelegation is IVestedDelegation, Vesting, Delegation, Ve ) internal virtual override { // If it is an vested delegation, withdraw by keeping the change in the delegation pool params // so vested rewards claiming is possible - if (vestedDelegationPositions[staker][delegator].isInVestingCycle()) { + if (vestedDelegationPositions[staker][delegator].isActive()) { return delegation.withdraw(delegator, amount, hydraChainContract.getCurrentEpochId()); } super._withdrawDelegation(staker, delegation, delegator, amount); } - /** - * @inheritdoc Delegation - */ - function _undelegate(address staker, address delegator, uint256 amount) internal virtual override { - super._undelegate(staker, delegator, amount); - } - /** * @notice Checks if the position has no reward conditions * @param position The vesting position diff --git a/contracts/HydraStaking/HydraStaking.sol b/contracts/HydraStaking/HydraStaking.sol index 3b5fac13..cfbbf5b3 100644 --- a/contracts/HydraStaking/HydraStaking.sol +++ b/contracts/HydraStaking/HydraStaking.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.17; +import {Unauthorized} from "../common/Errors.sol"; import {System} from "../common/System/System.sol"; import {SafeMathUint} from "./../common/libs/SafeMathUint.sol"; import {VestingPosition} from "../common/Vesting/IVesting.sol"; @@ -14,8 +15,6 @@ import {PenalizeableStaking} from "./modules/PenalizeableStaking/PenalizeableSta import {IHydraStaking, StakerInit} from "./IHydraStaking.sol"; import {Staking, IStaking} from "./Staking.sol"; -// TODO: An optimization we can do is keeping only once the general apr params for a block so we don' have to keep them for every single user - contract HydraStaking is IHydraStaking, System, @@ -29,7 +28,8 @@ contract HydraStaking is { using SafeMathUint for uint256; - uint256 public lastDistribution; // last rewards distribution timestamp + /// @notice last rewards distribution timestamp + uint256 public lastDistribution; /// @notice Mapping used to keep the paid rewards per epoch mapping(uint256 => uint256) public distributedRewardPerEpoch; @@ -134,10 +134,20 @@ contract HydraStaking is // _______________ Internal functions _______________ + /** + * @notice Check if the ban is initiated for the given account + * @param account The address of the account + */ + function _isBanInitiated(address account) internal view returns (bool) { + return hydraChainContract.banIsInitiated(account); + } + /** * @inheritdoc Staking */ function _stake(address account, uint256 amount) internal override(Staking, LiquidStaking, StateSyncStaking) { + if (_isBanInitiated(account)) revert Unauthorized("BAN_INITIATED"); + if (stakeOf(account) == 0) { hydraChainContract.activateValidator(account); } @@ -156,6 +166,8 @@ contract HydraStaking is override(Staking, VestedStaking, StateSyncStaking, LiquidStaking) returns (uint256 stakeLeft, uint256 withdrawAmount) { + if (_isBanInitiated(account)) revert Unauthorized("BAN_INITIATED"); + (stakeLeft, withdrawAmount) = super._unstake(account, amount); if (stakeLeft == 0) { hydraChainContract.deactivateValidator(account); @@ -193,7 +205,7 @@ contract HydraStaking is * @inheritdoc PenalizeableStaking */ function _afterPenalizeStakerHook(address staker, uint256 unstakeAmount, uint256 leftForStaker) internal override { - // the unstake amount of liquid tokens must be paid at the time of withdrawal + // the unstake amount of liquid tokens must be paid at the time of initiatePenalizedFundsWithdrawal // but only the leftForStaker will be automatically requested, // so we have to set the unstake amount - leftForStaker as liquidity debt that must be paid as well liquidityDebts[staker] += (unstakeAmount - leftForStaker).toInt256Safe(); @@ -233,12 +245,15 @@ contract HydraStaking is */ function _distributeTokens(address staker, uint256 amount) internal virtual override { VestingPosition memory position = vestedStakingPositions[staker]; + // This check works because if position has already been opened, the restrictions on stake() and stakeWithVesting() + // will prevent entering the check again if (_isOpeningPosition(position)) { - uint256 currentStake = stakeOf(staker); - if (currentStake != amount) { - currentStake -= amount; - _collectTokens(staker, currentStake); - amount += currentStake; + uint256 previousStake = stakeOf(staker) - amount; + if (previousStake != 0) { + // We want all previously distributed tokens to be collected, + // because for vested positions we distribute decreased amount of liquid tokens + _collectTokens(staker, previousStake); + amount += previousStake; } uint256 debt = _calculatePositionDebt(amount, position.duration); @@ -266,7 +281,7 @@ contract HydraStaking is /** * @notice Distributes the reward for the given staker. - * @notice Validator won't receive a reward in the epoch of exiting his position (stake becomes 0). His delegators will receive a reward for his uptime. + * @notice Validator won't receive a reward in the epoch of exiting the staking (stake becomes 0). His delegators will receive a reward for his uptime. * @param epochId The epoch id * @param uptime The uptime data for the validator (staker) * @param fullRewardIndex The full reward index @@ -276,13 +291,13 @@ contract HydraStaking is */ function _distributeReward( uint256 epochId, - Uptime calldata uptime, + Uptime memory uptime, uint256 fullRewardIndex, uint256 totalSupply, uint256 totalBlocks ) private returns (uint256 reward) { if (uptime.signedBlocks > totalBlocks) { - revert DistributeRewardFailed("SIGNED_BLOCKS_EXCEEDS_TOTAL"); + uptime.signedBlocks = totalBlocks; } uint256 currentStake = stakeOf(uptime.validator); @@ -296,14 +311,16 @@ contract HydraStaking is stakerRewardIndex ); - _distributeStakingReward(uptime.validator, stakerShares); - _distributeDelegationRewards(uptime.validator, delegatorShares, epochId); - - // Keep history record of the staker rewards to be used on maturing vesting reward claim if (stakerShares != 0) { + _distributeStakingReward(uptime.validator, stakerShares); + // Keep history record of the staker rewards to be used on maturing vesting reward claim _saveStakerRewardData(uptime.validator, epochId); } + if (delegatorShares != 0) { + _distributeDelegationRewards(uptime.validator, delegatorShares, epochId); + } + return stakerRewardIndex; } @@ -318,8 +335,10 @@ contract HydraStaking is uint256 delegatedBalance, uint256 totalReward ) private pure returns (uint256, uint256) { - if (stakedBalance == 0) return (0, totalReward); + // first check if delegated balance is zero + // otherwise if both staked and delegated are zero = reward will be lost if (delegatedBalance == 0) return (totalReward, 0); + if (stakedBalance == 0) return (0, totalReward); uint256 stakerReward = (totalReward * stakedBalance) / (stakedBalance + delegatedBalance); uint256 delegatorReward = totalReward - stakerReward; @@ -330,7 +349,7 @@ contract HydraStaking is * Calculates the epoch reward index. * We call it index because it is not the actual reward * but only the macroFactor and the "time passed from last distribution / 365 days ratio" are applied here. - * we need to apply the ration because all APR params are yearly + * we need to apply the ratio because all APR params are yearly * but we distribute rewards only for the time that has passed from last distribution. * The participation factor is applied later in the distribution process. * (base + vesting and RSI are applied on claimReward for delegators diff --git a/contracts/HydraStaking/Staking.sol b/contracts/HydraStaking/Staking.sol index c3ca82f0..c3bf56b5 100644 --- a/contracts/HydraStaking/Staking.sol +++ b/contracts/HydraStaking/Staking.sol @@ -84,7 +84,7 @@ contract Staking is IStaking, Withdrawal, HydraChainConnector, APRCalculatorConn * @inheritdoc IStaking */ function claimStakingRewards() public { - rewardWalletContract.distributeReward(msg.sender, _claimStakingRewards(msg.sender)); + _claimStakingRewards(msg.sender); } /** @@ -110,8 +110,6 @@ contract Staking is IStaking, Withdrawal, HydraChainConnector, APRCalculatorConn * @param amount The amount to stake */ function _stake(address account, uint256 amount) internal virtual { - if (_isBanInitiated(account)) revert Unauthorized("BAN_INITIATED"); - uint256 currentBalance = stakeOf(account); if (amount + currentBalance < minStake) revert StakeRequirement({src: "stake", msg: "STAKE_TOO_LOW"}); @@ -131,8 +129,6 @@ contract Staking is IStaking, Withdrawal, HydraChainConnector, APRCalculatorConn address account, uint256 amount ) internal virtual returns (uint256 stakeLeft, uint256 withdrawAmount) { - if (_isBanInitiated(account)) revert Unauthorized("BAN_INITIATED"); - uint256 accountStake = stakeOf(account); if (amount > accountStake) revert StakeRequirement({src: "unstake", msg: "INSUFFICIENT_BALANCE"}); @@ -151,7 +147,6 @@ contract Staking is IStaking, Withdrawal, HydraChainConnector, APRCalculatorConn /** * @notice Function that calculates the end reward for a user (without vesting bonuses) based on the pool reward index. - * @dev Denominator is used because we should work with floating-point numbers * @param rewardIndex index The reward index that we apply the base APR to * @dev The reward with the applied APR */ @@ -174,15 +169,9 @@ contract Staking is IStaking, Withdrawal, HydraChainConnector, APRCalculatorConn stakingRewards[staker].taken += rewards; - emit StakingRewardsClaimed(staker, rewards); - } + rewardWalletContract.distributeReward(staker, rewards); - /** - * @notice Check if the ban is initiated for the given account - * @param account The address of the account - */ - function _isBanInitiated(address account) internal view returns (bool) { - return hydraChainContract.banIsInitiated(account); + emit StakingRewardsClaimed(staker, rewards); } // _______________ Private functions _______________ diff --git a/contracts/HydraStaking/modules/VestedStaking/IVestedStaking.sol b/contracts/HydraStaking/modules/VestedStaking/IVestedStaking.sol index 53b12fd2..0921fabd 100644 --- a/contracts/HydraStaking/modules/VestedStaking/IVestedStaking.sol +++ b/contracts/HydraStaking/modules/VestedStaking/IVestedStaking.sol @@ -57,10 +57,10 @@ interface IVestedStaking is IVesting, IStaking { * @param staker The address of the staker * @param amount The amount that is going to be unstaked * @return penalty for the staker - * @return reward of the staker + * @return rewardToBurn of the staker */ function calcVestedStakingPositionPenalty( address staker, uint256 amount - ) external view returns (uint256 penalty, uint256 reward); + ) external view returns (uint256 penalty, uint256 rewardToBurn); } diff --git a/contracts/HydraStaking/modules/VestedStaking/VestedStaking.sol b/contracts/HydraStaking/modules/VestedStaking/VestedStaking.sol index 13d69956..72b9e700 100644 --- a/contracts/HydraStaking/modules/VestedStaking/VestedStaking.sol +++ b/contracts/HydraStaking/modules/VestedStaking/VestedStaking.sol @@ -46,7 +46,7 @@ abstract contract VestedStaking is IVestedStaking, Vesting, Staking { if (vestedStakingPositions[msg.sender].isInVestingCycle()) { revert StakeRequirement({src: "stakeWithVesting", msg: "ALREADY_IN_VESTING_CYCLE"}); } - // Claim the rewards before opening a new position, to avoid locking them during vesting cycle + if (unclaimedRewards(msg.sender) != 0) _claimStakingRewards(msg.sender); // Clear the staking rewards history @@ -136,13 +136,12 @@ abstract contract VestedStaking is IVestedStaking, Vesting, Staking { function calcVestedStakingPositionPenalty( address staker, uint256 amount - ) external view returns (uint256 penalty, uint256 reward) { - reward = stakingRewards[staker].total - stakingRewards[staker].taken; + ) external view returns (uint256 penalty, uint256 rewardToBurn) { VestingPosition memory position = vestedStakingPositions[staker]; if (position.isActive()) { penalty = _calcPenalty(position, amount); // if active position, reward is burned - reward = 0; + rewardToBurn = stakingRewards[staker].total - stakingRewards[staker].taken; } } diff --git a/contracts/LiquidityToken/LiquidityToken.sol b/contracts/LiquidityToken/LiquidityToken.sol index 2c96ab19..de9e567a 100644 --- a/contracts/LiquidityToken/LiquidityToken.sol +++ b/contracts/LiquidityToken/LiquidityToken.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.17; -import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import {ERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; import {System} from "../common/System/System.sol"; @@ -12,7 +11,7 @@ import {ILiquidityToken} from "./ILiquidityToken.sol"; * @title LiquidityToken * @dev This contract represents the liquid token for the Hydra staking mechanism. */ -contract LiquidityToken is ILiquidityToken, System, ERC20Upgradeable, ERC20PermitUpgradeable, Governed { +contract LiquidityToken is ILiquidityToken, System, ERC20PermitUpgradeable, Governed { /// @notice The role identifier for address(es) that have permission to mint and burn the token. bytes32 public constant SUPPLY_CONTROLLER_ROLE = keccak256("SUPPLY_CONTROLLER_ROLE"); diff --git a/contracts/VestingManager/VestingManagerFactory.sol b/contracts/VestingManager/VestingManagerFactory.sol index f76cf38e..4e922dcd 100644 --- a/contracts/VestingManager/VestingManagerFactory.sol +++ b/contracts/VestingManager/VestingManagerFactory.sol @@ -40,7 +40,7 @@ contract VestingManagerFactory is IVestingManagerFactory, System, Initializable BeaconProxy manager = new BeaconProxy( address(beacon), - abi.encodeWithSelector(VestingManager(address(0)).initialize.selector, msg.sender) + abi.encodeWithSelector(VestingManager.initialize.selector, msg.sender) ); _storeVestingManagerData(address(manager), msg.sender); diff --git a/contracts/common/Liquid/Liquid.sol b/contracts/common/Liquid/Liquid.sol index 3071f481..1edd592d 100644 --- a/contracts/common/Liquid/Liquid.sol +++ b/contracts/common/Liquid/Liquid.sol @@ -54,15 +54,14 @@ abstract contract Liquid is ILiquid, Initializable { // _______________ Internal functions _______________ - function _collectTokens(address account, uint256 amount) internal virtual { + function _collectTokens(address account, uint256 amount) internal { // User needs to provide their liquid tokens debt as well int256 liquidDebt = liquidityDebts[account]; int256 amountInt = amount.toInt256Safe(); int256 amountAfterDebt = amountInt + liquidDebt; - // if negative debt is bigger than or equal to the amount, so we get the whole amount from the debt + // if a negative debt covers the whole amount, no need to burn anything if (amountAfterDebt < 1) { liquidityDebts[account] -= amountInt; - amount = 0; return; } diff --git a/contracts/common/Vesting/VestedPositionLib.sol b/contracts/common/Vesting/VestedPositionLib.sol index f180e1f1..895db040 100644 --- a/contracts/common/Vesting/VestedPositionLib.sol +++ b/contracts/common/Vesting/VestedPositionLib.sol @@ -26,7 +26,7 @@ library VestedPositionLib { } /** - * @notice Returns true if the staker/delegator is an active vesting position or not all rewards from the latest active position are matured yet + * @notice Returns true if the staker/delegator is an active vesting position or the max maturing period is not yet reached * @param position Vesting position * @return bool Returns true if the position is in vesting cycle */ diff --git a/docs/HydraChain/HydraChain.md b/docs/HydraChain/HydraChain.md index 06d78323..dd4be789 100644 --- a/docs/HydraChain/HydraChain.md +++ b/docs/HydraChain/HydraChain.md @@ -223,7 +223,7 @@ Returns if a ban process is initiated for a given validator function banThreshold() external view returns (uint256) ``` -Validator inactiveness (in seconds) threshold that needs to be passed to ban a validator +Threshold for validator inactiveness (in seconds). A validator can be banned if it remains in the ban-initiated state for a duration equal to or exceeding this threshold. @@ -729,7 +729,7 @@ Method used to initiate a ban for validator, if the initiate ban threshold is re function initiateBanThreshold() external view returns (uint256) ``` -Validator inactiveness (in blocks) threshold that needs to be passed to initiate ban for a validator +Threshold for validator inactiveness (in blocks). A ban can be initiated for a validator if this threshold is reached or exceeded. diff --git a/docs/HydraChain/modules/Inspector/Inspector.md b/docs/HydraChain/modules/Inspector/Inspector.md index 7aea17a9..b073f2eb 100644 --- a/docs/HydraChain/modules/Inspector/Inspector.md +++ b/docs/HydraChain/modules/Inspector/Inspector.md @@ -223,7 +223,7 @@ Returns if a ban process is initiated for a given validator function banThreshold() external view returns (uint256) ``` -Validator inactiveness (in seconds) threshold that needs to be passed to ban a validator +Threshold for validator inactiveness (in seconds). A validator can be banned if it remains in the ban-initiated state for a duration equal to or exceeding this threshold. @@ -479,9 +479,9 @@ Method used to initiate a ban for validator, if the initiate ban threshold is re function initiateBanThreshold() external view returns (uint256) ``` -Validator inactiveness (in blocks) threshold that needs to be passed to initiate ban for a validator - +Threshold for validator inactiveness (in blocks). A ban can be initiated for a validator if this threshold is reached or exceeded. +*must be always bigger than the epoch length (better bigger than at least 4 epochs), otherwise all validators can be banned* #### Returns diff --git a/docs/HydraChain/modules/ValidatorManager/ValidatorManager.md b/docs/HydraChain/modules/ValidatorManager/ValidatorManager.md index 22e0bcab..96544970 100644 --- a/docs/HydraChain/modules/ValidatorManager/ValidatorManager.md +++ b/docs/HydraChain/modules/ValidatorManager/ValidatorManager.md @@ -694,7 +694,7 @@ function validatorsParticipation(address) external view returns (uint256) Mapping that keeps the last time when a validator has participated in the consensus -*Keep in mind that the validator will initially be set active when stake, but it will be able to participate in the next epoch. So, the validator will have less blocks to participate before getting eligible for ban.* +*Updated on epoch-ending blocks onlyKeep in mind that the validator will initially be set active when stake, but it will be able to participate in the next epoch. So, the validator will have less blocks to participate before getting eligible for ban.* #### Parameters diff --git a/docs/HydraDelegation/HydraDelegation.md b/docs/HydraDelegation/HydraDelegation.md index a2e0f7fc..0161aed4 100644 --- a/docs/HydraDelegation/HydraDelegation.md +++ b/docs/HydraDelegation/HydraDelegation.md @@ -208,23 +208,6 @@ function aprCalculatorContract() external view returns (contract IAPRCalculator) |---|---|---| | _0 | contract IAPRCalculator | undefined | -### balanceChangeThreshold - -```solidity -function balanceChangeThreshold() external view returns (uint256) -``` - -The threshold for the maximum number of allowed balance changes - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### calculateOwedLiquidTokens ```solidity diff --git a/docs/HydraDelegation/modules/VestedDelegation/VestedDelegation.md b/docs/HydraDelegation/modules/VestedDelegation/VestedDelegation.md index b5fd5b69..8bde5f3b 100644 --- a/docs/HydraDelegation/modules/VestedDelegation/VestedDelegation.md +++ b/docs/HydraDelegation/modules/VestedDelegation/VestedDelegation.md @@ -106,23 +106,6 @@ function aprCalculatorContract() external view returns (contract IAPRCalculator) |---|---|---| | _0 | contract IAPRCalculator | undefined | -### balanceChangeThreshold - -```solidity -function balanceChangeThreshold() external view returns (uint256) -``` - -The threshold for the maximum number of allowed balance changes - -*We are using this to restrict unlimited changes of the balance (delegationPoolParamsHistory)* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### calculatePositionClaimableReward ```solidity diff --git a/docs/HydraStaking/HydraStaking.md b/docs/HydraStaking/HydraStaking.md index af8def1c..9ef30bbc 100644 --- a/docs/HydraStaking/HydraStaking.md +++ b/docs/HydraStaking/HydraStaking.md @@ -183,7 +183,7 @@ function aprCalculatorContract() external view returns (contract IAPRCalculator) ### calcVestedStakingPositionPenalty ```solidity -function calcVestedStakingPositionPenalty(address staker, uint256 amount) external view returns (uint256 penalty, uint256 reward) +function calcVestedStakingPositionPenalty(address staker, uint256 amount) external view returns (uint256 penalty, uint256 rewardToBurn) ``` Returns the penalty and reward that will be burned, if vested stake position is active @@ -202,7 +202,7 @@ Returns the penalty and reward that will be burned, if vested stake position is | Name | Type | Description | |---|---|---| | penalty | uint256 | for the staker | -| reward | uint256 | of the staker | +| rewardToBurn | uint256 | of the staker | ### calculateOwedLiquidTokens @@ -528,7 +528,7 @@ Register withdrawal of the penalized funds function lastDistribution() external view returns (uint256) ``` - +last rewards distribution timestamp diff --git a/docs/HydraStaking/IHydraStaking.md b/docs/HydraStaking/IHydraStaking.md index 19b86e8e..69ebf825 100644 --- a/docs/HydraStaking/IHydraStaking.md +++ b/docs/HydraStaking/IHydraStaking.md @@ -13,7 +13,7 @@ ### calcVestedStakingPositionPenalty ```solidity -function calcVestedStakingPositionPenalty(address staker, uint256 amount) external view returns (uint256 penalty, uint256 reward) +function calcVestedStakingPositionPenalty(address staker, uint256 amount) external view returns (uint256 penalty, uint256 rewardToBurn) ``` Returns the penalty and reward that will be burned, if vested stake position is active @@ -32,7 +32,7 @@ Returns the penalty and reward that will be burned, if vested stake position is | Name | Type | Description | |---|---|---| | penalty | uint256 | for the staker | -| reward | uint256 | of the staker | +| rewardToBurn | uint256 | of the staker | ### calculateOwedLiquidTokens diff --git a/docs/HydraStaking/modules/VestedStaking/IVestedStaking.md b/docs/HydraStaking/modules/VestedStaking/IVestedStaking.md index 34b882aa..b4d3690a 100644 --- a/docs/HydraStaking/modules/VestedStaking/IVestedStaking.md +++ b/docs/HydraStaking/modules/VestedStaking/IVestedStaking.md @@ -13,7 +13,7 @@ ### calcVestedStakingPositionPenalty ```solidity -function calcVestedStakingPositionPenalty(address staker, uint256 amount) external view returns (uint256 penalty, uint256 reward) +function calcVestedStakingPositionPenalty(address staker, uint256 amount) external view returns (uint256 penalty, uint256 rewardToBurn) ``` Returns the penalty and reward that will be burned, if vested stake position is active @@ -32,7 +32,7 @@ Returns the penalty and reward that will be burned, if vested stake position is | Name | Type | Description | |---|---|---| | penalty | uint256 | for the staker | -| reward | uint256 | of the staker | +| rewardToBurn | uint256 | of the staker | ### calculatePositionClaimableReward diff --git a/docs/HydraStaking/modules/VestedStaking/VestedStaking.md b/docs/HydraStaking/modules/VestedStaking/VestedStaking.md index fd7916f8..46f3234e 100644 --- a/docs/HydraStaking/modules/VestedStaking/VestedStaking.md +++ b/docs/HydraStaking/modules/VestedStaking/VestedStaking.md @@ -81,7 +81,7 @@ function aprCalculatorContract() external view returns (contract IAPRCalculator) ### calcVestedStakingPositionPenalty ```solidity -function calcVestedStakingPositionPenalty(address staker, uint256 amount) external view returns (uint256 penalty, uint256 reward) +function calcVestedStakingPositionPenalty(address staker, uint256 amount) external view returns (uint256 penalty, uint256 rewardToBurn) ``` Returns the penalty and reward that will be burned, if vested stake position is active @@ -100,7 +100,7 @@ Returns the penalty and reward that will be burned, if vested stake position is | Name | Type | Description | |---|---|---| | penalty | uint256 | for the staker | -| reward | uint256 | of the staker | +| rewardToBurn | uint256 | of the staker | ### calculatePositionClaimableReward diff --git a/test/HydraDelegation/VestedDelegation.test.ts b/test/HydraDelegation/VestedDelegation.test.ts index 020e6659..c2bb831c 100644 --- a/test/HydraDelegation/VestedDelegation.test.ts +++ b/test/HydraDelegation/VestedDelegation.test.ts @@ -232,6 +232,12 @@ export function RunVestedDelegationTests(): void { this.fixtures.vestedDelegationFixture ); + const delegatedAmount = await hydraDelegation.delegationOf(this.delegatedValidators[0], vestManager.address); + const amountToCut = delegatedAmount.div(2); + const amount = await hydraDelegation.calculateOwedLiquidTokens(vestManager.address, amountToCut); + await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); + await vestManager.connect(vestManagerOwner).cutVestedDelegatePosition(this.delegatedValidators[0], amountToCut); + // go in the maturing phase await time.increase(WEEK * 16); @@ -246,22 +252,16 @@ export function RunVestedDelegationTests(): void { ); expect(mature, "mature").to.be.true; - const delegatedAmount = await hydraDelegation.delegationOf(this.delegatedValidators[0], vestManager.address); - const amountToDelegate = this.minDelegation.mul(2); - const amount = await hydraDelegation.calculateOwedLiquidTokens(vestManager.address, delegatedAmount.div(2)); - await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); - await vestManager - .connect(vestManagerOwner) - .cutVestedDelegatePosition(this.delegatedValidators[0], delegatedAmount.div(2)); - // check if the balance change is made const epochNum = await hydraChain.getCurrentEpochId(); - const isBalanceMadeChange = await hydraDelegation.isBalanceChangeMade( + const isBalanceChangeMade = await hydraDelegation.isBalanceChangeMade( this.delegatedValidators[0], vestManager.address, epochNum ); - expect(isBalanceMadeChange, "isBalanceMadeChange").to.be.true; + expect(isBalanceChangeMade, "isBalanceChangeMade").to.be.true; + + const amountToDelegate = this.minDelegation.mul(2); const tx = await vestManager.openVestedDelegatePosition(this.delegatedValidators[0], vestingDuration, { value: amountToDelegate, }); @@ -272,7 +272,7 @@ export function RunVestedDelegationTests(): void { vestManager.address, this.delegatedValidators[0], vestingDuration, - amountToDelegate.add(delegatedAmount.div(2)) + amountToDelegate.add(delegatedAmount.sub(amountToCut)) ); expect(await hydraDelegation.delegationOf(this.delegatedValidators[0], vestManager.address)).to.be.equal( diff --git a/test/HydraStaking/HydraStaking.test.ts b/test/HydraStaking/HydraStaking.test.ts index a5519f6b..2f089837 100644 --- a/test/HydraStaking/HydraStaking.test.ts +++ b/test/HydraStaking/HydraStaking.test.ts @@ -297,13 +297,13 @@ export function RunHydraStakingTests(): void { const reward = await hydraStaking.unclaimedRewards(rewardingValidator.address); - await expect( - hydraStaking.connect(rewardingValidator).stakeWithVesting(3, { - value: this.minStake, - }) - ) - .to.emit(hydraStaking, "StakingRewardsClaimed") - .withArgs(rewardingValidator.address, reward); + const tx = hydraStaking.connect(rewardingValidator).stakeWithVesting(3, { + value: this.minStake, + }); + + await expect(tx).to.emit(hydraStaking, "StakingRewardsClaimed").withArgs(rewardingValidator.address, reward); + + await expect(tx).to.changeEtherBalance(rewardingValidator.address, this.minStake.sub(reward).mul(-1)); expect(await hydraStaking.unclaimedRewards(rewardingValidator.address)).to.be.equal(0); }); diff --git a/test/HydraStaking/VestedStaking.test.ts b/test/HydraStaking/VestedStaking.test.ts index 26630808..658d8594 100644 --- a/test/HydraStaking/VestedStaking.test.ts +++ b/test/HydraStaking/VestedStaking.test.ts @@ -61,15 +61,30 @@ export function RunVestedStakingTests(): void { }); it("should open vested position with the old stake base and adjust token balance", async function () { - const { hydraStaking, liquidToken } = await loadFixture(this.fixtures.stakedValidatorsStateFixture); + const { hydraStaking, systemHydraChain, liquidToken } = await loadFixture( + this.fixtures.stakedValidatorsStateFixture + ); + + await commitEpochs( + systemHydraChain, + hydraStaking, + [this.signers.validators[0], this.signers.validators[1], this.staker], + 5, // number of epochs to commit + this.epochSize + ); const validator = this.signers.validators[0]; await hydraStaking.connect(validator).stake({ value: this.minStake.mul(20) }); const tokenBalance = await liquidToken.balanceOf(validator.address); + const rewardBeforeOpeningVestedPosition = await hydraStaking.unclaimedRewards(validator.address); + expect(rewardBeforeOpeningVestedPosition).to.be.gt(0); - await hydraStaking.connect(validator).stakeWithVesting(52, { value: this.minStake }); + // stake with vesting must distribute rewards from the previous stake + await expect( + hydraStaking.connect(validator).stakeWithVesting(52, { value: this.minStake }) + ).to.changeEtherBalance(validator.address, this.minStake.sub(rewardBeforeOpeningVestedPosition).mul(-1)); const currentStake = await hydraStaking.stakeOf(validator.address); const expectedLiquidTokens = calcLiquidTokensToDistributeOnVesting(52, currentStake); @@ -228,7 +243,7 @@ export function RunVestedStakingTests(): void { const position = await hydraStaking.vestedStakingPositions(this.staker.address); const latestTimestamp = hre.ethers.BigNumber.from(await time.latest()); // get the penalty and reward from the contract - const { penalty, reward } = await hydraStaking.calcVestedStakingPositionPenalty( + const { penalty, rewardToBurn } = await hydraStaking.calcVestedStakingPositionPenalty( this.staker.address, this.minStake ); @@ -238,7 +253,7 @@ export function RunVestedStakingTests(): void { expect(penalty, "penalty").to.be.gt(0); expect(penalty, "penalty = calculatedPenalty").to.be.equal(calculatedPenalty); - expect(reward, "reward").to.be.equal(0); // if active position, reward is burned + expect(rewardToBurn, "rewardToBurn").to.be.gt(0); // if active position, reward is burned }); it("should decrease staking position and apply slashing penalty", async function () {