From 7c5531a599f86e750e4cc15e36a4c2541fd0b204 Mon Sep 17 00:00:00 2001 From: Samuil Borisov Date: Mon, 20 May 2024 18:43:43 +0300 Subject: [PATCH 1/7] add function for swap --- contracts/RewardPool/IRewardPool.sol | 13 ++++ .../RewardPool/modules/DelegationRewards.sol | 60 +++++++++++++++++++ .../modules/Delegation/Delegation.sol | 11 ++++ .../modules/Delegation/IDelegation.sol | 8 +++ .../modules/Delegation/IVestedDelegation.sol | 1 + docs/RewardPool/IRewardPool.md | 24 ++++++++ docs/RewardPool/RewardPool.md | 24 ++++++++ docs/RewardPool/RewardPoolBase.md | 24 ++++++++ docs/RewardPool/modules/DelegationRewards.md | 24 ++++++++ docs/RewardPool/modules/StakingRewards.md | 24 ++++++++ docs/ValidatorSet/ValidatorSet.md | 36 +++++++++++ .../modules/Delegation/Delegation.md | 36 +++++++++++ .../modules/Delegation/IDelegation.md | 17 ++++++ .../modules/Delegation/IVestedDelegation.md | 19 ++++++ .../modules/Delegation/VestedDelegation.md | 19 ++++++ test/RewardPool/RewardPool.test.ts | 49 +++++++++++++++ test/ValidatorSet/Delegation.test.ts | 5 ++ 17 files changed, 394 insertions(+) diff --git a/contracts/RewardPool/IRewardPool.sol b/contracts/RewardPool/IRewardPool.sol index eadcfafc..3136706e 100644 --- a/contracts/RewardPool/IRewardPool.sol +++ b/contracts/RewardPool/IRewardPool.sol @@ -136,6 +136,19 @@ interface IRewardPool { uint256 currentEpochId ) external returns (uint256 penalty, uint256 fullReward); + /** + * @notice Swap a vesting postion from one validator to another + * @param oldValidator The address of the validator to swap from + * @param newValidator The address of the delegator to swap to + * @param delegator The address of the delegator + * @return amount The swapped amount + */ + function onSwapPosition( + address oldValidator, + address newValidator, + address delegator + ) external returns (uint256 amount); + /** * @notice Claims delegator rewards for sender. * @param validator Validator to claim from diff --git a/contracts/RewardPool/modules/DelegationRewards.sol b/contracts/RewardPool/modules/DelegationRewards.sol index 2375933c..b78d8e74 100644 --- a/contracts/RewardPool/modules/DelegationRewards.sol +++ b/contracts/RewardPool/modules/DelegationRewards.sol @@ -302,6 +302,66 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa } } + /** + * @inheritdoc IRewardPool + */ + function onSwapPosition( + address oldValidator, + address newValidator, + address delegator + ) external onlyValidatorSet returns (uint256 amount) { + DelegationPool storage delegation = delegationPools[newValidator]; + uint256 balance = delegation.balanceOf(delegator); + + VestingPosition memory position = delegationPositions[newValidator][delegator]; + if (position.isMaturing()) { + revert DelegateRequirement({src: "vesting", msg: "POSITION_MATURING"}); + } + + if (position.isActive()) { + revert DelegateRequirement({src: "vesting", msg: "POSITION_ACTIVE"}); + } + + // ensure previous rewards are claimed + if (delegation.claimableRewards(delegator) > 0) { + revert DelegateRequirement({src: "vesting", msg: "REWARDS_NOT_CLAIMED"}); + } + + DelegationPool storage oldDelegation = delegationPools[oldValidator]; + amount = oldDelegation.balanceOf(delegator); + oldDelegation.withdraw(delegator, amount); + + uint256 newBalance = balance + amount; + VestingPosition memory oldPosition = delegationPositions[oldValidator][delegator]; + // If is a position which is not active and not in maturing state, + // we can recreate/create the position + delegation.deposit(delegator, newBalance); + delete delegationPoolParamsHistory[newValidator][delegator]; + delete beforeTopUpParams[newValidator][delegator]; + + // TODO: calculate end of period instead of write in in the cold storage. It is cheaper + delegationPositions[newValidator][delegator] = VestingPosition({ + duration: oldPosition.duration, + start: oldPosition.start, + end: oldPosition.end, + base: oldPosition.base, + vestBonus: oldPosition.vestBonus, + rsiBonus: oldPosition.rsiBonus + }); + + // keep the change in the delegation pool params per account + _addNewDelegationPoolParam( + newValidator, + delegator, + DelegationPoolParams({ + balance: newBalance, + correction: delegation.correctionOf(delegator), + epochNum: 1 // TODO: Check if this is correct + }) + ); + + } + /** * @inheritdoc IRewardPool */ diff --git a/contracts/ValidatorSet/modules/Delegation/Delegation.sol b/contracts/ValidatorSet/modules/Delegation/Delegation.sol index 0f788f5b..90d52779 100644 --- a/contracts/ValidatorSet/modules/Delegation/Delegation.sol +++ b/contracts/ValidatorSet/modules/Delegation/Delegation.sol @@ -79,6 +79,17 @@ abstract contract Delegation is emit PositionCut(msg.sender, validator, amountAfterPenalty); } + /** + * @inheritdoc IDelegation + */ + function swapVestedValidator(address oldValidator, address newValidator) external onlyManager { + uint256 amount = rewardPool.onSwapPosition(oldValidator, newValidator, msg.sender); + _undelegate(oldValidator, msg.sender, amount); + _delegate(newValidator, msg.sender, amount); + + emit PositionSwapped(msg.sender, oldValidator, newValidator, amount); + } + // _______________ Private functions _______________ function _delegate(address validator, address delegator, uint256 amount) private onlyActiveValidator(validator) { diff --git a/contracts/ValidatorSet/modules/Delegation/IDelegation.sol b/contracts/ValidatorSet/modules/Delegation/IDelegation.sol index 82dceafe..7370e507 100644 --- a/contracts/ValidatorSet/modules/Delegation/IDelegation.sol +++ b/contracts/ValidatorSet/modules/Delegation/IDelegation.sol @@ -43,4 +43,12 @@ interface IDelegation { * @param amount Amount to be undelegated */ function undelegateWithVesting(address validator, uint256 amount) external; + + /** + * @notice Swaps the validator for a vesting position. + * Can be called by vesting positions' managers only. + * @param oldValidator Validator to swap from + * @param newValidator Validator to swap to + */ + function swapVestedValidator(address oldValidator, address newValidator) external; } diff --git a/contracts/ValidatorSet/modules/Delegation/IVestedDelegation.sol b/contracts/ValidatorSet/modules/Delegation/IVestedDelegation.sol index cbf6e27f..8fab0318 100644 --- a/contracts/ValidatorSet/modules/Delegation/IVestedDelegation.sol +++ b/contracts/ValidatorSet/modules/Delegation/IVestedDelegation.sol @@ -10,6 +10,7 @@ interface IVestedDelegation { ); event PositionTopUp(address indexed manager, address indexed validator, uint256 amount); event PositionCut(address indexed manager, address indexed validator, uint256 amount); + event PositionSwapped(address indexed manager, address indexed oldValidator, address newValidator, uint256 amount); /// @notice Gets the vesting managers per user address for fast off-chain lookup. function getUserVestManagers(address user) external view returns (address[] memory); diff --git a/docs/RewardPool/IRewardPool.md b/docs/RewardPool/IRewardPool.md index cbbfe10b..5be257eb 100644 --- a/docs/RewardPool/IRewardPool.md +++ b/docs/RewardPool/IRewardPool.md @@ -404,6 +404,30 @@ Update the reward params for the vested position | amount | uint256 | Amount to stake | | oldBalance | uint256 | Balance before stake | +### onSwapPosition + +```solidity +function onSwapPosition(address oldValidator, address newValidator, address delegator) external nonpayable returns (uint256 amount) +``` + +Swap a vesting postion from one validator to another + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| oldValidator | address | The address of the validator to swap from | +| newValidator | address | The address of the delegator to swap to | +| delegator | address | The address of the delegator | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| amount | uint256 | The swapped amount | + ### onTopUpDelegatePosition ```solidity diff --git a/docs/RewardPool/RewardPool.md b/docs/RewardPool/RewardPool.md index 63cc0557..4645f46b 100644 --- a/docs/RewardPool/RewardPool.md +++ b/docs/RewardPool/RewardPool.md @@ -1212,6 +1212,30 @@ Update the reward params for the vested position | amount | uint256 | Amount to stake | | oldBalance | uint256 | Balance before stake | +### onSwapPosition + +```solidity +function onSwapPosition(address oldValidator, address newValidator, address delegator) external nonpayable returns (uint256 amount) +``` + +Swap a vesting postion from one validator to another + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| oldValidator | address | The address of the validator to swap from | +| newValidator | address | The address of the delegator to swap to | +| delegator | address | The address of the delegator | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| amount | uint256 | The swapped amount | + ### onTopUpDelegatePosition ```solidity diff --git a/docs/RewardPool/RewardPoolBase.md b/docs/RewardPool/RewardPoolBase.md index 1777db32..a07a1543 100644 --- a/docs/RewardPool/RewardPoolBase.md +++ b/docs/RewardPool/RewardPoolBase.md @@ -404,6 +404,30 @@ Update the reward params for the vested position | amount | uint256 | Amount to stake | | oldBalance | uint256 | Balance before stake | +### onSwapPosition + +```solidity +function onSwapPosition(address oldValidator, address newValidator, address delegator) external nonpayable returns (uint256 amount) +``` + +Swap a vesting postion from one validator to another + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| oldValidator | address | The address of the validator to swap from | +| newValidator | address | The address of the delegator to swap to | +| delegator | address | The address of the delegator | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| amount | uint256 | The swapped amount | + ### onTopUpDelegatePosition ```solidity diff --git a/docs/RewardPool/modules/DelegationRewards.md b/docs/RewardPool/modules/DelegationRewards.md index 4c307320..bc63e70a 100644 --- a/docs/RewardPool/modules/DelegationRewards.md +++ b/docs/RewardPool/modules/DelegationRewards.md @@ -1064,6 +1064,30 @@ Update the reward params for the vested position | amount | uint256 | Amount to stake | | oldBalance | uint256 | Balance before stake | +### onSwapPosition + +```solidity +function onSwapPosition(address oldValidator, address newValidator, address delegator) external nonpayable returns (uint256 amount) +``` + +Swap a vesting postion from one validator to another + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| oldValidator | address | The address of the validator to swap from | +| newValidator | address | The address of the delegator to swap to | +| delegator | address | The address of the delegator | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| amount | uint256 | The swapped amount | + ### onTopUpDelegatePosition ```solidity diff --git a/docs/RewardPool/modules/StakingRewards.md b/docs/RewardPool/modules/StakingRewards.md index e19f1e17..a7f2e88e 100644 --- a/docs/RewardPool/modules/StakingRewards.md +++ b/docs/RewardPool/modules/StakingRewards.md @@ -1008,6 +1008,30 @@ Update the reward params for the vested position | amount | uint256 | Amount to stake | | oldBalance | uint256 | Balance before stake | +### onSwapPosition + +```solidity +function onSwapPosition(address oldValidator, address newValidator, address delegator) external nonpayable returns (uint256 amount) +``` + +Swap a vesting postion from one validator to another + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| oldValidator | address | The address of the validator to swap from | +| newValidator | address | The address of the delegator to swap to | +| delegator | address | The address of the delegator | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| amount | uint256 | The swapped amount | + ### onTopUpDelegatePosition ```solidity diff --git a/docs/ValidatorSet/ValidatorSet.md b/docs/ValidatorSet/ValidatorSet.md index 0e470bd4..c63ca56a 100644 --- a/docs/ValidatorSet/ValidatorSet.md +++ b/docs/ValidatorSet/ValidatorSet.md @@ -959,6 +959,23 @@ Stakes sent amount with vesting period. |---|---|---| | durationWeeks | uint256 | Duration of the vesting in weeks. Must be between 1 and 52. | +### swapVestedValidator + +```solidity +function swapVestedValidator(address oldValidator, address newValidator) external nonpayable +``` + +Swaps the validator for a vesting position. Can be called by vesting positions' managers only. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| oldValidator | address | Validator to swap from | +| newValidator | address | Validator to swap to | + ### topUpDelegatePosition ```solidity @@ -1526,6 +1543,25 @@ event PositionOpened(address indexed manager, address indexed validator, uint256 | weeksDuration `indexed` | uint256 | undefined | | amount | uint256 | undefined | +### PositionSwapped + +```solidity +event PositionSwapped(address indexed manager, address indexed oldValidator, address newValidator, uint256 amount) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| manager `indexed` | address | undefined | +| oldValidator `indexed` | address | undefined | +| newValidator | address | undefined | +| amount | uint256 | undefined | + ### PositionTopUp ```solidity diff --git a/docs/ValidatorSet/modules/Delegation/Delegation.md b/docs/ValidatorSet/modules/Delegation/Delegation.md index c41700ac..3a947659 100644 --- a/docs/ValidatorSet/modules/Delegation/Delegation.md +++ b/docs/ValidatorSet/modules/Delegation/Delegation.md @@ -538,6 +538,23 @@ function stakeBalances(address) external view returns (uint256) |---|---|---| | _0 | uint256 | undefined | +### swapVestedValidator + +```solidity +function swapVestedValidator(address oldValidator, address newValidator) external nonpayable +``` + +Swaps the validator for a vesting position. Can be called by vesting positions' managers only. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| oldValidator | address | Validator to swap from | +| newValidator | address | Validator to swap to | + ### topUpDelegatePosition ```solidity @@ -988,6 +1005,25 @@ event PositionOpened(address indexed manager, address indexed validator, uint256 | weeksDuration `indexed` | uint256 | undefined | | amount | uint256 | undefined | +### PositionSwapped + +```solidity +event PositionSwapped(address indexed manager, address indexed oldValidator, address newValidator, uint256 amount) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| manager `indexed` | address | undefined | +| oldValidator `indexed` | address | undefined | +| newValidator | address | undefined | +| amount | uint256 | undefined | + ### PositionTopUp ```solidity diff --git a/docs/ValidatorSet/modules/Delegation/IDelegation.md b/docs/ValidatorSet/modules/Delegation/IDelegation.md index 09c69d20..0a5e6460 100644 --- a/docs/ValidatorSet/modules/Delegation/IDelegation.md +++ b/docs/ValidatorSet/modules/Delegation/IDelegation.md @@ -43,6 +43,23 @@ Delegates sent amount to validator. Set vesting position data. Delete old top-up | validator | address | Validator to delegate to | | durationWeeks | uint256 | Duration of the vesting in weeks | +### swapVestedValidator + +```solidity +function swapVestedValidator(address oldValidator, address newValidator) external nonpayable +``` + +Swaps the validator for a vesting position. Can be called by vesting positions' managers only. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| oldValidator | address | Validator to swap from | +| newValidator | address | Validator to swap to | + ### topUpDelegatePosition ```solidity diff --git a/docs/ValidatorSet/modules/Delegation/IVestedDelegation.md b/docs/ValidatorSet/modules/Delegation/IVestedDelegation.md index d6abd4ae..a10b9b57 100644 --- a/docs/ValidatorSet/modules/Delegation/IVestedDelegation.md +++ b/docs/ValidatorSet/modules/Delegation/IVestedDelegation.md @@ -89,6 +89,25 @@ event PositionOpened(address indexed manager, address indexed validator, uint256 | weeksDuration `indexed` | uint256 | undefined | | amount | uint256 | undefined | +### PositionSwapped + +```solidity +event PositionSwapped(address indexed manager, address indexed oldValidator, address newValidator, uint256 amount) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| manager `indexed` | address | undefined | +| oldValidator `indexed` | address | undefined | +| newValidator | address | undefined | +| amount | uint256 | undefined | + ### PositionTopUp ```solidity diff --git a/docs/ValidatorSet/modules/Delegation/VestedDelegation.md b/docs/ValidatorSet/modules/Delegation/VestedDelegation.md index 4dfc6846..39399104 100644 --- a/docs/ValidatorSet/modules/Delegation/VestedDelegation.md +++ b/docs/ValidatorSet/modules/Delegation/VestedDelegation.md @@ -206,6 +206,25 @@ event PositionOpened(address indexed manager, address indexed validator, uint256 | weeksDuration `indexed` | uint256 | undefined | | amount | uint256 | undefined | +### PositionSwapped + +```solidity +event PositionSwapped(address indexed manager, address indexed oldValidator, address newValidator, uint256 amount) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| manager `indexed` | address | undefined | +| oldValidator `indexed` | address | undefined | +| newValidator | address | undefined | +| amount | uint256 | undefined | + ### PositionTopUp ```solidity diff --git a/test/RewardPool/RewardPool.test.ts b/test/RewardPool/RewardPool.test.ts index 294657bf..f188cbed 100644 --- a/test/RewardPool/RewardPool.test.ts +++ b/test/RewardPool/RewardPool.test.ts @@ -1115,3 +1115,52 @@ export function RunVestedDelegateClaimTests(): void { }); }); } + +export function RunVestedDelegationSwapTests(): void { + describe.only("Delegate position rewards", async function () { + it("should swap vested delegation", async function () { + const { validatorSet, systemValidatorSet, rewardPool, vestManager, vestManagerOwner } = await loadFixture( + this.fixtures.vestManagerFixture + ); + + const validator = this.signers.validators[2]; + const vestingDuration = 11; // 11 weeks + await vestManager.openVestedDelegatePosition(validator.address, vestingDuration, { + value: this.minDelegation.mul(2), + }); + + // Commit epochs so rewards to be distributed + await commitEpochs( + systemValidatorSet, + rewardPool, + [this.signers.validators[0], this.signers.validators[1], validator], + 5, // number of epochs to commit + this.epochSize + ); + + // 5 weeks ahead + await time.increase(WEEK * 5); + + // commit epoch, so more reward is added that must not be claimed now + await commitEpoch( + systemValidatorSet, + rewardPool, + [this.signers.validators[0], this.signers.validators[1], validator], + this.epochSize + ); + + const newValidator = this.signers.validators[3]; + await validatorSet.connect(vestManagerOwner).swapVestedValidator(validator.address, newValidator.address); + + // prepare params for call + const { epochNum, topUpIndex } = await retrieveRPSData( + validatorSet, + rewardPool, + validator.address, + vestManager.address + ); + + vestManager.connect(vestManagerOwner).claimVestedPositionReward(validator.address, epochNum, topUpIndex); + }); + }); +} diff --git a/test/ValidatorSet/Delegation.test.ts b/test/ValidatorSet/Delegation.test.ts index 48d7f17c..feb0cb2c 100644 --- a/test/ValidatorSet/Delegation.test.ts +++ b/test/ValidatorSet/Delegation.test.ts @@ -10,6 +10,7 @@ import { calculatePenalty, claimPositionRewards, commitEpoch, commitEpochs, getU import { RunDelegateClaimTests, RunVestedDelegateClaimTests, + RunVestedDelegationSwapTests, RunVestedDelegationRewardsTests, RunDelegateFunctionsByValidatorSet, } from "../RewardPool/RewardPool.test"; @@ -862,6 +863,10 @@ export function RunDelegationTests(): void { RunVestedDelegateClaimTests(); }); + describe("Reward Pool - Vested delegate swap", async function () { + RunVestedDelegationSwapTests(); + }); + describe("Reward Pool - ValidatorSet protected delegate functions", function () { RunDelegateFunctionsByValidatorSet(); }); From ff23f26a41b33a25b6d96b81a6e7f08ec5271bf5 Mon Sep 17 00:00:00 2001 From: Samuil Borisov Date: Tue, 21 May 2024 19:35:50 +0300 Subject: [PATCH 2/7] fix contract logic, add more testing --- contracts/RewardPool/IRewardPool.sol | 11 +- .../RewardPool/modules/DelegationRewards.sol | 28 ++-- .../modules/Delegation/Delegation.sol | 2 +- .../modules/Delegation/VestManager.sol | 7 + docs/RewardPool/IRewardPool.md | 26 +++- docs/RewardPool/RewardPool.md | 26 +++- docs/RewardPool/RewardPoolBase.md | 26 +++- docs/RewardPool/modules/DelegationRewards.md | 26 +++- docs/RewardPool/modules/StakingRewards.md | 26 +++- .../modules/Delegation/VestManager.md | 17 +++ test/RewardPool/RewardPool.test.ts | 130 ++++++++++++++++-- 11 files changed, 296 insertions(+), 29 deletions(-) diff --git a/contracts/RewardPool/IRewardPool.sol b/contracts/RewardPool/IRewardPool.sol index 3136706e..7338812d 100644 --- a/contracts/RewardPool/IRewardPool.sol +++ b/contracts/RewardPool/IRewardPool.sol @@ -146,9 +146,18 @@ interface IRewardPool { function onSwapPosition( address oldValidator, address newValidator, - address delegator + address delegator, + uint256 currentEpochId ) external returns (uint256 amount); + /** + * @notice View function to see delegated vested amount + * @param validator The address of the validator + * @param delegator The address of the delegator + * @return reward Return the delegetared vested amount + */ + function getBalanceForVestedPosition(address validator, address delegator) external view returns (uint256); + /** * @notice Claims delegator rewards for sender. * @param validator Validator to claim from diff --git a/contracts/RewardPool/modules/DelegationRewards.sol b/contracts/RewardPool/modules/DelegationRewards.sol index b78d8e74..00d4fc01 100644 --- a/contracts/RewardPool/modules/DelegationRewards.sol +++ b/contracts/RewardPool/modules/DelegationRewards.sol @@ -308,7 +308,8 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa function onSwapPosition( address oldValidator, address newValidator, - address delegator + address delegator, + uint256 currentEpochId ) external onlyValidatorSet returns (uint256 amount) { DelegationPool storage delegation = delegationPools[newValidator]; uint256 balance = delegation.balanceOf(delegator); @@ -356,17 +357,9 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa DelegationPoolParams({ balance: newBalance, correction: delegation.correctionOf(delegator), - epochNum: 1 // TODO: Check if this is correct + epochNum: currentEpochId }) ); - - } - - /** - * @inheritdoc IRewardPool - */ - function claimDelegatorReward(address validator) public { - _claimDelegatorReward(validator, msg.sender); } /** @@ -440,6 +433,21 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa // _______________ Public functions _______________ + /** + * @inheritdoc IRewardPool + */ + function getBalanceForVestedPosition(address validator, address delegator) public view returns (uint256 amount) { + DelegationPool storage oldDelegation = delegationPools[validator]; + amount = oldDelegation.balanceOf(delegator); + } + + /** + * @inheritdoc IRewardPool + */ + function claimDelegatorReward(address validator) public { + _claimDelegatorReward(validator, msg.sender); + } + /** * @inheritdoc IRewardPool */ diff --git a/contracts/ValidatorSet/modules/Delegation/Delegation.sol b/contracts/ValidatorSet/modules/Delegation/Delegation.sol index 90d52779..16d0fc3b 100644 --- a/contracts/ValidatorSet/modules/Delegation/Delegation.sol +++ b/contracts/ValidatorSet/modules/Delegation/Delegation.sol @@ -83,7 +83,7 @@ abstract contract Delegation is * @inheritdoc IDelegation */ function swapVestedValidator(address oldValidator, address newValidator) external onlyManager { - uint256 amount = rewardPool.onSwapPosition(oldValidator, newValidator, msg.sender); + uint256 amount = rewardPool.onSwapPosition(oldValidator, newValidator, msg.sender, currentEpochId); _undelegate(oldValidator, msg.sender, amount); _delegate(newValidator, msg.sender, amount); diff --git a/contracts/ValidatorSet/modules/Delegation/VestManager.sol b/contracts/ValidatorSet/modules/Delegation/VestManager.sol index 1e5bd2a7..5b9307a7 100644 --- a/contracts/ValidatorSet/modules/Delegation/VestManager.sol +++ b/contracts/ValidatorSet/modules/Delegation/VestManager.sol @@ -53,6 +53,13 @@ contract VestManager is Initializable, OwnableUpgradeable { IDelegation(delegation).undelegateWithVesting(validator, amount); } + function swapVestedValidator(address oldValidator, address newValidator) external onlyOwner { + uint256 amount = IRewardPool(rewardPool).getBalanceForVestedPosition(oldValidator, address(this)); + _fulfillLiquidTokens(msg.sender, amount); + IDelegation(delegation).swapVestedValidator(oldValidator, newValidator); + _sendLiquidTokens(msg.sender, amount); + } + function claimVestedPositionReward( address validator, uint256 epochNumber, diff --git a/docs/RewardPool/IRewardPool.md b/docs/RewardPool/IRewardPool.md index 5be257eb..ffa3a559 100644 --- a/docs/RewardPool/IRewardPool.md +++ b/docs/RewardPool/IRewardPool.md @@ -173,6 +173,29 @@ function distributeRewardsFor(uint256 epochId, Uptime[] uptime, uint256 epochSiz | uptime | Uptime[] | undefined | | epochSize | uint256 | undefined | +### getBalanceForVestedPosition + +```solidity +function getBalanceForVestedPosition(address validator, address delegator) external view returns (uint256) +``` + +View function to see delegated vested amount + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| validator | address | The address of the validator | +| delegator | address | The address of the delegator | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | reward Return the delegetared vested amount | + ### getDelegationPoolParamsHistory ```solidity @@ -407,7 +430,7 @@ Update the reward params for the vested position ### onSwapPosition ```solidity -function onSwapPosition(address oldValidator, address newValidator, address delegator) external nonpayable returns (uint256 amount) +function onSwapPosition(address oldValidator, address newValidator, address delegator, uint256 currentEpochId) external nonpayable returns (uint256 amount) ``` Swap a vesting postion from one validator to another @@ -421,6 +444,7 @@ Swap a vesting postion from one validator to another | oldValidator | address | The address of the validator to swap from | | newValidator | address | The address of the delegator to swap to | | delegator | address | The address of the delegator | +| currentEpochId | uint256 | undefined | #### Returns diff --git a/docs/RewardPool/RewardPool.md b/docs/RewardPool/RewardPool.md index 4645f46b..e4088faf 100644 --- a/docs/RewardPool/RewardPool.md +++ b/docs/RewardPool/RewardPool.md @@ -598,6 +598,29 @@ function distributeRewardsFor(uint256 epochId, Uptime[] uptime, uint256 epochSiz | uptime | Uptime[] | undefined | | epochSize | uint256 | undefined | +### getBalanceForVestedPosition + +```solidity +function getBalanceForVestedPosition(address validator, address delegator) external view returns (uint256 amount) +``` + +View function to see delegated vested amount + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| validator | address | The address of the validator | +| delegator | address | The address of the delegator | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| amount | uint256 | reward Return the delegetared vested amount | + ### getDelegationPoolParamsHistory ```solidity @@ -1215,7 +1238,7 @@ Update the reward params for the vested position ### onSwapPosition ```solidity -function onSwapPosition(address oldValidator, address newValidator, address delegator) external nonpayable returns (uint256 amount) +function onSwapPosition(address oldValidator, address newValidator, address delegator, uint256 currentEpochId) external nonpayable returns (uint256 amount) ``` Swap a vesting postion from one validator to another @@ -1229,6 +1252,7 @@ Swap a vesting postion from one validator to another | oldValidator | address | The address of the validator to swap from | | newValidator | address | The address of the delegator to swap to | | delegator | address | The address of the delegator | +| currentEpochId | uint256 | undefined | #### Returns diff --git a/docs/RewardPool/RewardPoolBase.md b/docs/RewardPool/RewardPoolBase.md index a07a1543..ee82b254 100644 --- a/docs/RewardPool/RewardPoolBase.md +++ b/docs/RewardPool/RewardPoolBase.md @@ -173,6 +173,29 @@ function distributeRewardsFor(uint256 epochId, Uptime[] uptime, uint256 epochSiz | uptime | Uptime[] | undefined | | epochSize | uint256 | undefined | +### getBalanceForVestedPosition + +```solidity +function getBalanceForVestedPosition(address validator, address delegator) external view returns (uint256) +``` + +View function to see delegated vested amount + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| validator | address | The address of the validator | +| delegator | address | The address of the delegator | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | reward Return the delegetared vested amount | + ### getDelegationPoolParamsHistory ```solidity @@ -407,7 +430,7 @@ Update the reward params for the vested position ### onSwapPosition ```solidity -function onSwapPosition(address oldValidator, address newValidator, address delegator) external nonpayable returns (uint256 amount) +function onSwapPosition(address oldValidator, address newValidator, address delegator, uint256 currentEpochId) external nonpayable returns (uint256 amount) ``` Swap a vesting postion from one validator to another @@ -421,6 +444,7 @@ Swap a vesting postion from one validator to another | oldValidator | address | The address of the validator to swap from | | newValidator | address | The address of the delegator to swap to | | delegator | address | The address of the delegator | +| currentEpochId | uint256 | undefined | #### Returns diff --git a/docs/RewardPool/modules/DelegationRewards.md b/docs/RewardPool/modules/DelegationRewards.md index bc63e70a..c29d13a0 100644 --- a/docs/RewardPool/modules/DelegationRewards.md +++ b/docs/RewardPool/modules/DelegationRewards.md @@ -469,6 +469,29 @@ function distributeRewardsFor(uint256 epochId, Uptime[] uptime, uint256 epochSiz | uptime | Uptime[] | undefined | | epochSize | uint256 | undefined | +### getBalanceForVestedPosition + +```solidity +function getBalanceForVestedPosition(address validator, address delegator) external view returns (uint256 amount) +``` + +View function to see delegated vested amount + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| validator | address | The address of the validator | +| delegator | address | The address of the delegator | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| amount | uint256 | reward Return the delegetared vested amount | + ### getDelegationPoolParamsHistory ```solidity @@ -1067,7 +1090,7 @@ Update the reward params for the vested position ### onSwapPosition ```solidity -function onSwapPosition(address oldValidator, address newValidator, address delegator) external nonpayable returns (uint256 amount) +function onSwapPosition(address oldValidator, address newValidator, address delegator, uint256 currentEpochId) external nonpayable returns (uint256 amount) ``` Swap a vesting postion from one validator to another @@ -1081,6 +1104,7 @@ Swap a vesting postion from one validator to another | oldValidator | address | The address of the validator to swap from | | newValidator | address | The address of the delegator to swap to | | delegator | address | The address of the delegator | +| currentEpochId | uint256 | undefined | #### Returns diff --git a/docs/RewardPool/modules/StakingRewards.md b/docs/RewardPool/modules/StakingRewards.md index a7f2e88e..a815abe5 100644 --- a/docs/RewardPool/modules/StakingRewards.md +++ b/docs/RewardPool/modules/StakingRewards.md @@ -454,6 +454,29 @@ function distributeRewardsFor(uint256 epochId, Uptime[] uptime, uint256 epochSiz | uptime | Uptime[] | undefined | | epochSize | uint256 | undefined | +### getBalanceForVestedPosition + +```solidity +function getBalanceForVestedPosition(address validator, address delegator) external view returns (uint256) +``` + +View function to see delegated vested amount + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| validator | address | The address of the validator | +| delegator | address | The address of the delegator | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | reward Return the delegetared vested amount | + ### getDelegationPoolParamsHistory ```solidity @@ -1011,7 +1034,7 @@ Update the reward params for the vested position ### onSwapPosition ```solidity -function onSwapPosition(address oldValidator, address newValidator, address delegator) external nonpayable returns (uint256 amount) +function onSwapPosition(address oldValidator, address newValidator, address delegator, uint256 currentEpochId) external nonpayable returns (uint256 amount) ``` Swap a vesting postion from one validator to another @@ -1025,6 +1048,7 @@ Swap a vesting postion from one validator to another | oldValidator | address | The address of the validator to swap from | | newValidator | address | The address of the delegator to swap to | | delegator | address | The address of the delegator | +| currentEpochId | uint256 | undefined | #### Returns diff --git a/docs/ValidatorSet/modules/Delegation/VestManager.md b/docs/ValidatorSet/modules/Delegation/VestManager.md index 6e83df26..08b8458c 100644 --- a/docs/ValidatorSet/modules/Delegation/VestManager.md +++ b/docs/ValidatorSet/modules/Delegation/VestManager.md @@ -141,6 +141,23 @@ The reward pool address |---|---|---| | _0 | address | undefined | +### swapVestedValidator + +```solidity +function swapVestedValidator(address oldValidator, address newValidator) external nonpayable +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| oldValidator | address | undefined | +| newValidator | address | undefined | + ### topUpVestedDelegatePosition ```solidity diff --git a/test/RewardPool/RewardPool.test.ts b/test/RewardPool/RewardPool.test.ts index f188cbed..5ce2b1dd 100644 --- a/test/RewardPool/RewardPool.test.ts +++ b/test/RewardPool/RewardPool.test.ts @@ -1119,29 +1119,50 @@ export function RunVestedDelegateClaimTests(): void { export function RunVestedDelegationSwapTests(): void { describe.only("Delegate position rewards", async function () { it("should swap vested delegation", async function () { - const { validatorSet, systemValidatorSet, rewardPool, vestManager, vestManagerOwner } = await loadFixture( - this.fixtures.vestManagerFixture - ); + const { validatorSet, systemValidatorSet, rewardPool, vestManager, vestManagerOwner, liquidToken } = + await loadFixture(this.fixtures.vestManagerFixture); const validator = this.signers.validators[2]; const vestingDuration = 11; // 11 weeks - await vestManager.openVestedDelegatePosition(validator.address, vestingDuration, { + await vestManager.connect(vestManagerOwner).openVestedDelegatePosition(validator.address, vestingDuration, { value: this.minDelegation.mul(2), }); - // Commit epochs so rewards to be distributed - await commitEpochs( + // 5 weeks ahead + await time.increase(WEEK * 5); + + // commit epoch, so more reward is added that must not be claimed now + await commitEpoch( systemValidatorSet, rewardPool, [this.signers.validators[0], this.signers.validators[1], validator], - 5, // number of epochs to commit this.epochSize ); - // 5 weeks ahead - await time.increase(WEEK * 5); + const newValidator = this.signers.validators[1]; + const amount = await rewardPool + .connect(vestManagerOwner) + .getBalanceForVestedPosition(validator.address, vestManager.address); - // commit epoch, so more reward is added that must not be claimed now + // give allowance & swap + await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); + await vestManager.connect(vestManagerOwner).swapVestedValidator(validator.address, newValidator.address); + + // check rewards for new validator + const rewardsAfterSwap = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); + expect(rewardsAfterSwap).to.be.eq(0); + + // expect the rewards to be more than 0 for 1st validator + const rewardsAfterSwapOldValidator = await rewardPool.getRawDelegatorReward( + validator.address, + vestManager.address + ); + expect(rewardsAfterSwapOldValidator).to.be.not.eq(0); + + // 6 weeks ahead for position to start maturing + await time.increase(WEEK * 6); + + // commit epoch await commitEpoch( systemValidatorSet, rewardPool, @@ -1149,8 +1170,20 @@ export function RunVestedDelegationSwapTests(): void { this.epochSize ); - const newValidator = this.signers.validators[3]; - await validatorSet.connect(vestManagerOwner).swapVestedValidator(validator.address, newValidator.address); + // see if position is maturing + const position = await rewardPool.delegationPositions(validator.address, vestManager.address); + expect(position.end.lt(await time.latest()), "isMaturing").to.be.true; + + // 5 weeks ahead for position to claim matured rewards for 1st validator + await time.increase(WEEK * 5); + + // commit epoch + await commitEpoch( + systemValidatorSet, + rewardPool, + [this.signers.validators[0], this.signers.validators[1], validator], + this.epochSize + ); // prepare params for call const { epochNum, topUpIndex } = await retrieveRPSData( @@ -1160,7 +1193,80 @@ export function RunVestedDelegationSwapTests(): void { vestManager.address ); + // expect the rewards to be more than 0 for 1st validator + const rewardsBeforeClaim = await rewardPool.getRawDelegatorReward(validator.address, vestManager.address); + expect(rewardsBeforeClaim).to.be.not.eq(0); + vestManager.connect(vestManagerOwner).claimVestedPositionReward(validator.address, epochNum, topUpIndex); + + // commit epoch, to distribute rewards + await commitEpoch( + systemValidatorSet, + rewardPool, + [this.signers.validators[0], this.signers.validators[1], validator], + this.epochSize + ); + + // expect the rewards to be 0 for 1st validator + const rewardsAfterClaim = await rewardPool.getRawDelegatorReward(validator.address, vestManager.address); + + // 6 weeks ahead for position to be fully matured + await time.increase(WEEK * 6); + + // commit epoch + await commitEpoch( + systemValidatorSet, + rewardPool, + [this.signers.validators[0], this.signers.validators[1], validator], + this.epochSize + ); + // see if position is matured + const newPosition = await rewardPool.delegationPositions(newValidator.address, vestManager.address); + expect(newPosition.end.add(newPosition.duration).lt(await time.latest()), "isMatured").to.be.true; + + // expect new position to be like the old position + expect(position.duration).to.be.eq(newPosition.duration); + expect(position.start).to.be.eq(newPosition.start); + expect(position.end).to.be.eq(newPosition.end); + expect(position.base).to.be.eq(newPosition.base); + expect(position.vestBonus).to.be.eq(newPosition.vestBonus); + expect(position.rsiBonus).to.be.eq(newPosition.rsiBonus); + + // expect the rewards to be still 0 for 1st validator + const rewardsAfterMatured = await rewardPool.getRawDelegatorReward(validator.address, vestManager.address); + expect(rewardsAfterClaim).to.be.eq(rewardsAfterMatured).and.to.be.eq(0); + + // expect the new rewards for new validator to be more than rewards for 1st validator before claim + const rewardsAfterMaturedNew = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); + expect(rewardsAfterMaturedNew).to.be.gt(rewardsBeforeClaim).and.to.be.gt(0); + + // balance before claim + const balanceBefore = await vestManagerOwner.getBalance(); + + // prepare params for call + const { epochNum: epochNumNew, topUpIndex: topUpIndexNew } = await retrieveRPSData( + validatorSet, + rewardPool, + newValidator.address, + vestManager.address + ); + vestManager.connect(vestManagerOwner).claimVestedPositionReward(newValidator.address, epochNumNew, topUpIndexNew); + + // commit epoch, so more reward is added that must not be claimed now + await commitEpoch( + systemValidatorSet, + rewardPool, + [this.signers.validators[0], this.signers.validators[1], validator], + this.epochSize + ); + + // expect the rewards to be 0 for new validator after claim + const rewardsAfterClaimNew = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); + expect(rewardsAfterClaimNew).to.be.eq(0); + + // check balance after claim + const balanceAfter = await vestManagerOwner.getBalance(); + expect(balanceAfter).to.be.gt(balanceBefore); }); }); } From f3df59a5ccdfcaba28157526047a7972dc94eeda Mon Sep 17 00:00:00 2001 From: Vitomir Pavlov Date: Wed, 5 Jun 2024 15:51:14 +0300 Subject: [PATCH 3/7] fix bugs when swap and optimize fix bugs and optimize the swap function for the vested delegations; create new function isBalanceChangeThresholdExceeded in order to check for the max amount of swaps (balance changes) allowed; new findTopUpIndex, getClosestMaturedTimestamp and hasMatured functions for the tests; adapt the tests, skip the top-up ones with adding todo comment to delete, and add some additional ones to cover cases; --- contracts/RewardPool/IRewardPool.sol | 17 +- .../RewardPool/libs/DelegationPoolLib.sol | 7 +- .../RewardPool/modules/DelegationRewards.sol | 84 +-- contracts/RewardPool/modules/Vesting.sol | 6 + .../modules/Delegation/VestManager.sol | 2 +- docs/RewardPool/IRewardPool.md | 39 +- docs/RewardPool/RewardPool.md | 83 ++- docs/RewardPool/RewardPoolBase.md | 39 +- docs/RewardPool/modules/DelegationRewards.md | 100 ++-- docs/RewardPool/modules/StakingRewards.md | 56 +- docs/RewardPool/modules/Vesting.md | 17 + docs/console.md | 12 + test/RewardPool/RewardPool.test.ts | 493 +++++++++++------- test/ValidatorSet/Delegation.test.ts | 12 +- test/constants.ts | 3 +- test/fixtures.ts | 37 ++ test/helper.ts | 74 ++- test/mochaContext.ts | 13 + 18 files changed, 707 insertions(+), 387 deletions(-) create mode 100644 docs/console.md diff --git a/contracts/RewardPool/IRewardPool.sol b/contracts/RewardPool/IRewardPool.sol index 7338812d..575f2233 100644 --- a/contracts/RewardPool/IRewardPool.sol +++ b/contracts/RewardPool/IRewardPool.sol @@ -150,14 +150,6 @@ interface IRewardPool { uint256 currentEpochId ) external returns (uint256 amount); - /** - * @notice View function to see delegated vested amount - * @param validator The address of the validator - * @param delegator The address of the delegator - * @return reward Return the delegetared vested amount - */ - function getBalanceForVestedPosition(address validator, address delegator) external view returns (uint256); - /** * @notice Claims delegator rewards for sender. * @param validator Validator to claim from @@ -274,9 +266,16 @@ interface IRewardPool { function totalDelegationOf(address validator) external view returns (uint256); /** - * @dev Should be called only by the Governance. * @notice Changes the minDelegationAmount + * @dev Should be called only by the Governance. * @param newMinDelegation New minimum delegation amount */ function changeMinDelegation(uint256 newMinDelegation) external; + + /** + * @notice Changes the threshold for the balance change + * @dev Should be called only by the Governance. + * @param newBalanceChangeThreshold The number of allowed changes of the balance + */ + function changeBalanceChangeThreshold(uint256 newBalanceChangeThreshold) external; } diff --git a/contracts/RewardPool/libs/DelegationPoolLib.sol b/contracts/RewardPool/libs/DelegationPoolLib.sol index 13f71a78..23d5ae2d 100644 --- a/contracts/RewardPool/libs/DelegationPoolLib.sol +++ b/contracts/RewardPool/libs/DelegationPoolLib.sol @@ -131,8 +131,11 @@ library DelegationPoolLib { uint256 balance, int256 correction ) internal view returns (uint256) { - if (pool.claimedRewards[account] >= rewardsEarned(rps, balance, correction)) return 0; - return rewardsEarned(rps, balance, correction) - pool.claimedRewards[account]; + uint256 _rewardsEarned = rewardsEarned(rps, balance, correction); + uint256 claimedRewards = pool.claimedRewards[account]; + if (claimedRewards >= _rewardsEarned) return 0; + + return _rewardsEarned - claimedRewards; } function claimRewards( diff --git a/contracts/RewardPool/modules/DelegationRewards.sol b/contracts/RewardPool/modules/DelegationRewards.sol index 00d4fc01..1eb48304 100644 --- a/contracts/RewardPool/modules/DelegationRewards.sol +++ b/contracts/RewardPool/modules/DelegationRewards.sol @@ -7,7 +7,6 @@ import "./RewardsWithdrawal.sol"; import "./../RewardPoolBase.sol"; import "./../libs/DelegationPoolLib.sol"; import "./../libs/VestingPositionLib.sol"; - import "./../../common/Errors.sol"; abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawal { @@ -32,6 +31,7 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa function __DelegationRewards_init_unchained(uint256 newMinDelegation) internal onlyInitializing { _changeMinDelegation(newMinDelegation); + balanceChangeThreshold = 32; } // _______________ External functions _______________ @@ -311,36 +311,34 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa address delegator, uint256 currentEpochId ) external onlyValidatorSet returns (uint256 amount) { - DelegationPool storage delegation = delegationPools[newValidator]; - uint256 balance = delegation.balanceOf(delegator); - - VestingPosition memory position = delegationPositions[newValidator][delegator]; - if (position.isMaturing()) { - revert DelegateRequirement({src: "vesting", msg: "POSITION_MATURING"}); + VestingPosition memory oldPosition = delegationPositions[oldValidator][delegator]; + // ensure that the old position is active in order to continue the swap + if (!oldPosition.isActive()) { + revert DelegateRequirement({src: "vesting", msg: "OLD_POSITION_INACTIVE"}); } - if (position.isActive()) { - revert DelegateRequirement({src: "vesting", msg: "POSITION_ACTIVE"}); + VestingPosition memory newPosition = delegationPositions[newValidator][delegator]; + if (newPosition.isMaturing()) { + revert DelegateRequirement({src: "vesting", msg: "NEW_POSITION_MATURING"}); } - // ensure previous rewards are claimed - if (delegation.claimableRewards(delegator) > 0) { - revert DelegateRequirement({src: "vesting", msg: "REWARDS_NOT_CLAIMED"}); + DelegationPool storage newDelegation = delegationPools[newValidator]; + uint256 balance = newDelegation.balanceOf(delegator); + if (balance != 0) { + revert DelegateRequirement({src: "vesting", msg: "INVALID_NEW_POSITION"}); } - + + // update the old delegation position DelegationPool storage oldDelegation = delegationPools[oldValidator]; amount = oldDelegation.balanceOf(delegator); oldDelegation.withdraw(delegator, amount); - uint256 newBalance = balance + amount; - VestingPosition memory oldPosition = delegationPositions[oldValidator][delegator]; - // If is a position which is not active and not in maturing state, - // we can recreate/create the position - delegation.deposit(delegator, newBalance); - delete delegationPoolParamsHistory[newValidator][delegator]; - delete beforeTopUpParams[newValidator][delegator]; + int256 correction = oldDelegation.correctionOf(delegator); + _onAccountParamsChange(oldValidator, delegator, 0, correction, currentEpochId); + + // set the new delegation position using the old position parameters + newDelegation.deposit(delegator, amount); - // TODO: calculate end of period instead of write in in the cold storage. It is cheaper delegationPositions[newValidator][delegator] = VestingPosition({ duration: oldPosition.duration, start: oldPosition.start, @@ -350,13 +348,13 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa rsiBonus: oldPosition.rsiBonus }); - // keep the change in the delegation pool params per account + // keep the change in the new delegation pool params _addNewDelegationPoolParam( newValidator, delegator, DelegationPoolParams({ - balance: newBalance, - correction: delegation.correctionOf(delegator), + balance: amount, + correction: newDelegation.correctionOf(delegator), epochNum: currentEpochId }) ); @@ -431,16 +429,11 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa _changeMinDelegation(newMinDelegation); } - // _______________ Public functions _______________ - - /** - * @inheritdoc IRewardPool - */ - function getBalanceForVestedPosition(address validator, address delegator) public view returns (uint256 amount) { - DelegationPool storage oldDelegation = delegationPools[validator]; - amount = oldDelegation.balanceOf(delegator); + function changeBalanceChangeThreshold(uint256 newBalanceChangeThreshold) external onlyRole(DEFAULT_ADMIN_ROLE) { + balanceChangeThreshold = newBalanceChangeThreshold; } + // _______________ Public functions _______________ /** * @inheritdoc IRewardPool */ @@ -460,6 +453,8 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa /** * @notice Checks if balance change was already made in the current epoch * @param validator Validator to delegate to + * @param delegator Delegator that has delegated + * @param currentEpochNum Current epoch number */ function isBalanceChangeMade( address validator, @@ -479,6 +474,15 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa return false; } + /** + * @notice Checks if the balance changes exceeds the threshold + * @param validator Validator to delegate to + * @param delegator Delegator that has delegated + */ + function isBalanceChangeThresholdExceeded(address validator, address delegator) public view returns (bool) { + return delegationPoolParamsHistory[validator][delegator].length > balanceChangeThreshold; + } + // _______________ Private functions _______________ function _changeMinDelegation(uint256 newMinDelegation) private { @@ -592,8 +596,13 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa DelegationPoolParams memory params ) private { if (isBalanceChangeMade(validator, delegator, params.epochNum)) { - // Top up can be made only once per epoch - revert StakeRequirement({src: "_addNewDelegationPoolParam", msg: "BALANCE_CHANGE_ALREADY_MADE"}); + // balance can be changed only once per epoch + revert DelegateRequirement({src: "_addNewDelegationPoolParam", msg: "BALANCE_CHANGE_ALREADY_MADE"}); + } + + if (isBalanceChangeThresholdExceeded(validator, delegator)) { + // maximum amount of balance changes exceeded + revert DelegateRequirement({src: "_addNewDelegationPoolParam", msg: "BALANCE_CHANGES_EXCEEDED"}); } delegationPoolParamsHistory[validator][delegator].push(params); @@ -622,10 +631,15 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa uint256 currentEpochId ) private { if (isBalanceChangeMade(validator, delegator, currentEpochId)) { - // Top up can be made only once on epoch + // balance can be changed only once per epoch revert DelegateRequirement({src: "_onAccountParamsChange", msg: "BALANCE_CHANGE_ALREADY_MADE"}); } + if (isBalanceChangeThresholdExceeded(validator, delegator)) { + // maximum amount of balance changes exceeded + revert DelegateRequirement({src: "_onAccountParamsChange", msg: "BALANCE_CHANGES_EXCEEDED"}); + } + delegationPoolParamsHistory[validator][delegator].push( DelegationPoolParams({balance: balance, correction: correction, epochNum: currentEpochId}) ); diff --git a/contracts/RewardPool/modules/Vesting.sol b/contracts/RewardPool/modules/Vesting.sol index 47c10ba4..60b9ea72 100644 --- a/contracts/RewardPool/modules/Vesting.sol +++ b/contracts/RewardPool/modules/Vesting.sol @@ -69,6 +69,12 @@ abstract contract Vesting is APR { */ mapping(address => mapping(address => RewardParams)) public beforeTopUpParams; + /** + * @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; + // _______________ External functions _______________ function isActivePosition(address staker) external view returns (bool) { diff --git a/contracts/ValidatorSet/modules/Delegation/VestManager.sol b/contracts/ValidatorSet/modules/Delegation/VestManager.sol index 5b9307a7..cb589798 100644 --- a/contracts/ValidatorSet/modules/Delegation/VestManager.sol +++ b/contracts/ValidatorSet/modules/Delegation/VestManager.sol @@ -54,7 +54,7 @@ contract VestManager is Initializable, OwnableUpgradeable { } function swapVestedValidator(address oldValidator, address newValidator) external onlyOwner { - uint256 amount = IRewardPool(rewardPool).getBalanceForVestedPosition(oldValidator, address(this)); + uint256 amount = IRewardPool(rewardPool).delegationOf(oldValidator, address(this)); _fulfillLiquidTokens(msg.sender, amount); IDelegation(delegation).swapVestedValidator(oldValidator, newValidator); _sendLiquidTokens(msg.sender, amount); diff --git a/docs/RewardPool/IRewardPool.md b/docs/RewardPool/IRewardPool.md index ffa3a559..afc0901b 100644 --- a/docs/RewardPool/IRewardPool.md +++ b/docs/RewardPool/IRewardPool.md @@ -81,6 +81,22 @@ Returns the total reward that is generated for a position |---|---|---| | reward | uint256 | for the delegator | +### changeBalanceChangeThreshold + +```solidity +function changeBalanceChangeThreshold(uint256 newBalanceChangeThreshold) external nonpayable +``` + +Changes the threshold for the balance change + +*Should be called only by the Governance.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| newBalanceChangeThreshold | uint256 | The number of allowed changes of the balance | + ### changeMinDelegation ```solidity @@ -173,29 +189,6 @@ function distributeRewardsFor(uint256 epochId, Uptime[] uptime, uint256 epochSiz | uptime | Uptime[] | undefined | | epochSize | uint256 | undefined | -### getBalanceForVestedPosition - -```solidity -function getBalanceForVestedPosition(address validator, address delegator) external view returns (uint256) -``` - -View function to see delegated vested amount - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| validator | address | The address of the validator | -| delegator | address | The address of the delegator | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | reward Return the delegetared vested amount | - ### getDelegationPoolParamsHistory ```solidity diff --git a/docs/RewardPool/RewardPool.md b/docs/RewardPool/RewardPool.md index e4088faf..2003fc50 100644 --- a/docs/RewardPool/RewardPool.md +++ b/docs/RewardPool/RewardPool.md @@ -281,6 +281,23 @@ function applyMaxReward(uint256 reward) external view returns (uint256) |---|---|---| | reward | uint256 | undefined | +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +### balanceChangeThreshold + +```solidity +function balanceChangeThreshold() external view returns (uint256) +``` + +The threshold for the maximum number of allowed balance changes + + + + #### Returns | Name | Type | Description | @@ -400,6 +417,22 @@ Returns the total reward that is generated for a position |---|---|---| | reward | uint256 | for the delegator | +### changeBalanceChangeThreshold + +```solidity +function changeBalanceChangeThreshold(uint256 newBalanceChangeThreshold) external nonpayable +``` + +Changes the threshold for the balance change + +*Should be called only by the Governance.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| newBalanceChangeThreshold | uint256 | The number of allowed changes of the balance | + ### changeMinDelegation ```solidity @@ -598,29 +631,6 @@ function distributeRewardsFor(uint256 epochId, Uptime[] uptime, uint256 epochSiz | uptime | Uptime[] | undefined | | epochSize | uint256 | undefined | -### getBalanceForVestedPosition - -```solidity -function getBalanceForVestedPosition(address validator, address delegator) external view returns (uint256 amount) -``` - -View function to see delegated vested amount - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| validator | address | The address of the validator | -| delegator | address | The address of the delegator | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| amount | uint256 | reward Return the delegetared vested amount | - ### getDelegationPoolParamsHistory ```solidity @@ -1010,8 +1020,31 @@ Checks if balance change was already made in the current epoch | Name | Type | Description | |---|---|---| | validator | address | Validator to delegate to | -| delegator | address | undefined | -| currentEpochNum | uint256 | undefined | +| delegator | address | Delegator that has delegated | +| currentEpochNum | uint256 | Current epoch number | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined | + +### isBalanceChangeThresholdExceeded + +```solidity +function isBalanceChangeThresholdExceeded(address validator, address delegator) external view returns (bool) +``` + +Checks if the balance changes exceeds the threshold + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| validator | address | Validator to delegate to | +| delegator | address | Delegator that has delegated | #### Returns diff --git a/docs/RewardPool/RewardPoolBase.md b/docs/RewardPool/RewardPoolBase.md index ee82b254..8d1299f3 100644 --- a/docs/RewardPool/RewardPoolBase.md +++ b/docs/RewardPool/RewardPoolBase.md @@ -81,6 +81,22 @@ Returns the total reward that is generated for a position |---|---|---| | reward | uint256 | for the delegator | +### changeBalanceChangeThreshold + +```solidity +function changeBalanceChangeThreshold(uint256 newBalanceChangeThreshold) external nonpayable +``` + +Changes the threshold for the balance change + +*Should be called only by the Governance.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| newBalanceChangeThreshold | uint256 | The number of allowed changes of the balance | + ### changeMinDelegation ```solidity @@ -173,29 +189,6 @@ function distributeRewardsFor(uint256 epochId, Uptime[] uptime, uint256 epochSiz | uptime | Uptime[] | undefined | | epochSize | uint256 | undefined | -### getBalanceForVestedPosition - -```solidity -function getBalanceForVestedPosition(address validator, address delegator) external view returns (uint256) -``` - -View function to see delegated vested amount - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| validator | address | The address of the validator | -| delegator | address | The address of the delegator | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | reward Return the delegetared vested amount | - ### getDelegationPoolParamsHistory ```solidity diff --git a/docs/RewardPool/modules/DelegationRewards.md b/docs/RewardPool/modules/DelegationRewards.md index c29d13a0..e3da5c64 100644 --- a/docs/RewardPool/modules/DelegationRewards.md +++ b/docs/RewardPool/modules/DelegationRewards.md @@ -179,6 +179,23 @@ function applyMaxReward(uint256 reward) external view returns (uint256) |---|---|---| | reward | uint256 | undefined | +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +### balanceChangeThreshold + +```solidity +function balanceChangeThreshold() external view returns (uint256) +``` + +The threshold for the maximum number of allowed balance changes + + + + #### Returns | Name | Type | Description | @@ -298,6 +315,22 @@ Returns the total reward that is generated for a position |---|---|---| | reward | uint256 | for the delegator | +### changeBalanceChangeThreshold + +```solidity +function changeBalanceChangeThreshold(uint256 newBalanceChangeThreshold) external nonpayable +``` + +Changes the threshold for the balance change + +*Should be called only by the Governance.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| newBalanceChangeThreshold | uint256 | The number of allowed changes of the balance | + ### changeMinDelegation ```solidity @@ -469,29 +502,6 @@ function distributeRewardsFor(uint256 epochId, Uptime[] uptime, uint256 epochSiz | uptime | Uptime[] | undefined | | epochSize | uint256 | undefined | -### getBalanceForVestedPosition - -```solidity -function getBalanceForVestedPosition(address validator, address delegator) external view returns (uint256 amount) -``` - -View function to see delegated vested amount - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| validator | address | The address of the validator | -| delegator | address | The address of the delegator | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| amount | uint256 | reward Return the delegetared vested amount | - ### getDelegationPoolParamsHistory ```solidity @@ -862,8 +872,31 @@ Checks if balance change was already made in the current epoch | Name | Type | Description | |---|---|---| | validator | address | Validator to delegate to | -| delegator | address | undefined | -| currentEpochNum | uint256 | undefined | +| delegator | address | Delegator that has delegated | +| currentEpochNum | uint256 | Current epoch number | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined | + +### isBalanceChangeThresholdExceeded + +```solidity +function isBalanceChangeThresholdExceeded(address validator, address delegator) external view returns (bool) +``` + +Checks if the balance changes exceeds the threshold + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| validator | address | Validator to delegate to | +| delegator | address | Delegator that has delegated | #### Returns @@ -1628,23 +1661,6 @@ error InvalidRSI() -### StakeRequirement - -```solidity -error StakeRequirement(string src, string msg) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| src | string | undefined | -| msg | string | undefined | - ### Unauthorized ```solidity diff --git a/docs/RewardPool/modules/StakingRewards.md b/docs/RewardPool/modules/StakingRewards.md index a815abe5..e54accec 100644 --- a/docs/RewardPool/modules/StakingRewards.md +++ b/docs/RewardPool/modules/StakingRewards.md @@ -162,6 +162,23 @@ function applyMaxReward(uint256 reward) external view returns (uint256) |---|---|---| | reward | uint256 | undefined | +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +### balanceChangeThreshold + +```solidity +function balanceChangeThreshold() external view returns (uint256) +``` + +The threshold for the maximum number of allowed balance changes + + + + #### Returns | Name | Type | Description | @@ -281,6 +298,22 @@ Returns the total reward that is generated for a position |---|---|---| | reward | uint256 | for the delegator | +### changeBalanceChangeThreshold + +```solidity +function changeBalanceChangeThreshold(uint256 newBalanceChangeThreshold) external nonpayable +``` + +Changes the threshold for the balance change + +*Should be called only by the Governance.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| newBalanceChangeThreshold | uint256 | The number of allowed changes of the balance | + ### changeMinDelegation ```solidity @@ -454,29 +487,6 @@ function distributeRewardsFor(uint256 epochId, Uptime[] uptime, uint256 epochSiz | uptime | Uptime[] | undefined | | epochSize | uint256 | undefined | -### getBalanceForVestedPosition - -```solidity -function getBalanceForVestedPosition(address validator, address delegator) external view returns (uint256) -``` - -View function to see delegated vested amount - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| validator | address | The address of the validator | -| delegator | address | The address of the delegator | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | reward Return the delegetared vested amount | - ### getDelegationPoolParamsHistory ```solidity diff --git a/docs/RewardPool/modules/Vesting.md b/docs/RewardPool/modules/Vesting.md index a33fdec0..e1972161 100644 --- a/docs/RewardPool/modules/Vesting.md +++ b/docs/RewardPool/modules/Vesting.md @@ -162,6 +162,23 @@ function applyMaxReward(uint256 reward) external view returns (uint256) |---|---|---| | reward | uint256 | undefined | +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | 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 | diff --git a/docs/console.md b/docs/console.md new file mode 100644 index 00000000..8bab67a4 --- /dev/null +++ b/docs/console.md @@ -0,0 +1,12 @@ +# console + + + + + + + + + + + diff --git a/test/RewardPool/RewardPool.test.ts b/test/RewardPool/RewardPool.test.ts index 5ce2b1dd..b464afc7 100644 --- a/test/RewardPool/RewardPool.test.ts +++ b/test/RewardPool/RewardPool.test.ts @@ -3,7 +3,7 @@ import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers"; import * as hre from "hardhat"; import { expect } from "chai"; -import { EPOCHS_YEAR, ERRORS, MIN_RSI_BONUS, VESTING_DURATION_WEEKS, WEEK } from "../constants"; +import { DAY, EPOCHS_YEAR, ERRORS, MIN_RSI_BONUS, VESTING_DURATION_WEEKS, WEEK } from "../constants"; import { calculateExpectedReward, commitEpoch, @@ -282,16 +282,14 @@ export function RunVestedDelegationRewardsTests(): void { this.minDelegation ); - // pass two weeks ahead - await time.increase(WEEK * 2); - - // Commit epochs so rewards to be distributed + // commit epochs to distribute rewards await commitEpochs( systemValidatorSet, rewardPool, [this.signers.validators[0], validator], - 10, // number of epochs to commit - this.epochSize + 5, // number of epochs to commit + this.epochSize, + DAY * 3 // three days per epoch, so, 3 x 5 = 15 days ahead ); const managerRewards = await getDelegatorPositionReward( @@ -309,15 +307,13 @@ export function RunVestedDelegationRewardsTests(): void { this.fixtures.weeklyVestedDelegationFixture ); - // enter maturing period - await time.increase(WEEK * 1 + 1); - - // Commit epoch so some more rewards are distributed + // commit epoch so some more rewards are distributed await commitEpoch( systemValidatorSet, rewardPool, [this.signers.validators[0], delegatedValidator], - this.epochSize + this.epochSize, + WEEK + 1 ); const managerRewards = await getDelegatorPositionReward( @@ -355,16 +351,14 @@ export function RunVestedDelegationRewardsTests(): void { this.minDelegation ); - // pass two weeks ahead - await time.increase(WEEK * 2); - // Commit epochs so rewards to be distributed await commitEpochs( systemValidatorSet, rewardPool, [this.signers.validators[0], this.signers.validators[1], validator], - 10, // number of epochs to commit - this.epochSize + 5, // number of epochs to commit + this.epochSize, + DAY * 3 // three days per epoch, so, 3 x 5 = 15 days ahead ); const manager1rewards = await rewardPool.calculateTotalPositionReward(validator.address, manager1.address); @@ -394,16 +388,14 @@ export function RunVestedDelegationRewardsTests(): void { this.minDelegation ); - // pass three weeks ahead - await time.increase(WEEK * 3); - // Commit epochs so rewards to be distributed await commitEpochs( systemValidatorSet, rewardPool, [this.signers.validators[0], this.signers.validators[1], validator], - 20, // number of epochs to commit - this.epochSize + 7, // number of epochs to commit + this.epochSize, + DAY * 3 // three days per epoch, so, 3 x 7 = 21 days ahead ); const manager1rewards = await rewardPool.calculateTotalPositionReward(validator.address, manager1.address); @@ -433,16 +425,14 @@ export function RunVestedDelegationRewardsTests(): void { this.minDelegation ); - // pass five weeks ahead - await time.increase(WEEK * 5); - - // Commit epochs so rewards to be distributed + // commit epochs so rewards to be distributed await commitEpochs( systemValidatorSet, rewardPool, [this.signers.validators[0], this.signers.validators[1], validator], - 20, // number of epochs to commit - this.epochSize + 5, // number of epochs to commit + this.epochSize, + WEEK // one week = 1 epoch ); const manager1rewards = await rewardPool.calculateTotalPositionReward(validator.address, manager1.address); @@ -551,7 +541,7 @@ export function RunVestedDelegateClaimTests(): void { .withArgs("vesting", "WRONG_RPS"); }); - it("should properly claim reward when no top-ups and not full reward matured", async function () { + it("should properly claim reward when not fully matured", async function () { const { systemValidatorSet, validatorSet, rewardPool, vestManager, vestManagerOwner, delegatedValidator } = await loadFixture(this.fixtures.weeklyVestedDelegationFixture); @@ -567,15 +557,13 @@ export function RunVestedDelegateClaimTests(): void { const maxRSI = await rewardPool.MAX_RSI_BONUS(); const maxReward = await calculateExpectedReward(base, maxVestBonus, maxRSI, baseReward); - // enter the maturing state - await time.increase(WEEK * 1 + 1); - // commit epoch, so more reward is added that must not be claimed now await commitEpoch( systemValidatorSet, rewardPool, [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize + this.epochSize, + WEEK + 1 ); // prepare params for call @@ -595,7 +583,7 @@ export function RunVestedDelegateClaimTests(): void { ); }); - it("should properly claim reward when no top-ups and full reward matured", async function () { + it("should properly claim reward when position fully matured", async function () { const { systemValidatorSet, validatorSet, rewardPool, vestManager, vestManagerOwner, delegatedValidator } = await loadFixture(this.fixtures.weeklyVestedDelegationFixture); @@ -611,15 +599,13 @@ export function RunVestedDelegateClaimTests(): void { const maxRSI = await rewardPool.MAX_RSI_BONUS(); const maxReward = await calculateExpectedReward(base, maxVestBonus, maxRSI, baseReward); - // ensure maturing has finished - await time.increase(WEEK * 2 + 1); - - // more rewards to be distributed but with the top-up data + // more rewards to be distributed await commitEpoch( systemValidatorSet, rewardPool, [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize + this.epochSize, + WEEK * 2 + 1 ); const additionalReward = ( @@ -653,7 +639,8 @@ export function RunVestedDelegateClaimTests(): void { ); }); - it("should properly claim reward when top-ups and not full reward matured", async function () { + // TODO: Delete when remove the top-up functionality + it.skip("should properly claim reward when top-ups and not full reward matured", async function () { const { systemValidatorSet, validatorSet, rewardPool, vestManager, vestManagerOwner, delegatedValidator } = await loadFixture(this.fixtures.weeklyVestedDelegationFixture); @@ -668,6 +655,10 @@ export function RunVestedDelegateClaimTests(): void { await vestManager.topUpVestedDelegatePosition(delegatedValidator.address, { value: this.minDelegation, }); + + const topUpRewardsTimestamp = await time.latest(); + const position = await rewardPool.delegationPositions(delegatedValidator.address, vestManager.address); + const toBeMatured = hre.ethers.BigNumber.from(topUpRewardsTimestamp).sub(position.start); // more rewards to be distributed but with the top-up data await commitEpoch( systemValidatorSet, @@ -676,10 +667,6 @@ export function RunVestedDelegateClaimTests(): void { this.epochSize ); - const topUpRewardsTimestamp = await time.latest(); - const position = await rewardPool.delegationPositions(delegatedValidator.address, vestManager.address); - const toBeMatured = hre.ethers.BigNumber.from(topUpRewardsTimestamp).sub(position.start); - // calculate top-up reward const topUpReward = (await rewardPool.getRawDelegatorReward(delegatedValidator.address, vestManager.address)).sub( baseReward @@ -696,29 +683,25 @@ export function RunVestedDelegateClaimTests(): void { const maxTopUpReward = await calculateExpectedReward(base, maxVestBonus, maxRSI, topUpReward); const maxReward = maxBaseReward.add(maxTopUpReward); - // enter the maturing state - // two week is the duration + the needed time for the top-up to be matured - await time.increase(2 * WEEK + toBeMatured.toNumber() + 1); - // commit epoch, so more reward is added that must not be claimed now await commitEpoch( systemValidatorSet, rewardPool, [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize + this.epochSize, + // enter the maturing state + // two week is the duration + the needed time for the top-up to be matured + WEEK * 2 + toBeMatured.toNumber() + 1 ); // prepare params for call - let { epochNum, topUpIndex } = await retrieveRPSData( + const { epochNum, topUpIndex } = await retrieveRPSData( validatorSet, rewardPool, delegatedValidator.address, vestManager.address ); - // 1 because we have only one top-up - topUpIndex = 1; - const areRewardsMatured = position.end.add(toBeMatured).lt(await time.latest()); expect(areRewardsMatured, "areRewardsMatured").to.be.true; @@ -731,7 +714,8 @@ export function RunVestedDelegateClaimTests(): void { ); }); - it("should properly claim reward when top-ups and full reward matured", async function () { + // TODO: Delete when remove the top-up functionality + it.skip("should properly claim reward when top-ups and full reward matured", async function () { const { systemValidatorSet, validatorSet, rewardPool, vestManager, vestManagerOwner, delegatedValidator } = await loadFixture(this.fixtures.weeklyVestedDelegationFixture); @@ -769,16 +753,15 @@ export function RunVestedDelegateClaimTests(): void { const maxReward = maxBaseReward.add(maxTopUpReward); - // enter the maturing state - // 52 weeks is the duration + the needed time for the top-up to be matured - await time.increase(WEEK * 104 * 4 + 1); - // commit epoch, so more reward is added that must be without bonus await commitEpoch( systemValidatorSet, rewardPool, [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize + this.epochSize, + // enter the maturing state + // 52 weeks is the duration + the needed time for the top-up to be matured + WEEK * 104 * 4 + 1 ); const additionalReward = ( @@ -815,7 +798,8 @@ export function RunVestedDelegateClaimTests(): void { ); }); - it("should revert when invalid top-up index", async function () { + // TODO: Delete when remove the top-up functionality + it.skip("should revert when invalid top-up index", async function () { const { systemValidatorSet, validatorSet, rewardPool, vestManager, delegatedValidator } = await loadFixture( this.fixtures.weeklyVestedDelegationFixture ); @@ -835,16 +819,15 @@ export function RunVestedDelegateClaimTests(): void { const position = await rewardPool.delegationPositions(delegatedValidator.address, vestManager.address); const toBeMatured = hre.ethers.BigNumber.from(topUpRewardsTimestamp).sub(position.start); - // enter the maturing state - // two week is the duration + the needed time for the top-up to be matured - await time.increase(WEEK * 104 + toBeMatured.toNumber() + 1); - // comit epoch, so more reward is added that must not be claimed now await commitEpoch( systemValidatorSet, rewardPool, [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize + this.epochSize, + // enter the maturing state + // two week is the duration + the needed time for the top-up to be matured + WEEK * 104 + toBeMatured.toNumber() + 1 ); // prepare params for call @@ -867,7 +850,8 @@ export function RunVestedDelegateClaimTests(): void { .withArgs("vesting", "INVALID_TOP_UP_INDEX"); }); - it("should revert when later top-up index", async function () { + // TODO: Delete when remove the top-up functionality + it.skip("should revert when later top-up index", async function () { const { systemValidatorSet, validatorSet, rewardPool, vestManager, delegatedValidator } = await loadFixture( this.fixtures.weeklyVestedDelegationFixture ); @@ -894,16 +878,15 @@ export function RunVestedDelegateClaimTests(): void { this.epochSize ); - // enter the maturing state - // 52 weeks is the duration + the needed time for the top-up to be matured - await time.increase(WEEK * 104 + 1); - // commit epoch, so more reward is added that must not be claimed now await commitEpoch( systemValidatorSet, rewardPool, [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize + this.epochSize, + // enter the maturing state + // 52 weeks is the duration + the needed time for the top-up to be matured + WEEK * 104 + 1 ); // prepare params for call @@ -922,7 +905,8 @@ export function RunVestedDelegateClaimTests(): void { .withArgs("vesting", "LATER_TOP_UP"); }); - it("should revert when earlier top-up index", async function () { + // TODO: Delete when remove the top-up functionality + it.skip("should revert when earlier top-up index", async function () { const { systemValidatorSet, validatorSet, rewardPool, vestManager, delegatedValidator } = await loadFixture( this.fixtures.weeklyVestedDelegationFixture ); @@ -971,7 +955,8 @@ export function RunVestedDelegateClaimTests(): void { .withArgs("vesting", "EARLIER_TOP_UP"); }); - it("should claim only reward made before top-up", async function () { + // TODO: Delete when remove the top-up functionality + it.skip("should claim only reward made before top-up", async function () { const { systemValidatorSet, validatorSet, rewardPool, vestManager, vestManagerOwner, delegatedValidator } = await loadFixture(this.fixtures.weeklyVestedDelegationFixture); @@ -989,7 +974,7 @@ export function RunVestedDelegateClaimTests(): void { const rewardDistributionTime = await time.latest(); let position = await rewardPool.delegationPositions(delegatedValidator.address, vestManager.address); const toBeMatured = hre.ethers.BigNumber.from(rewardDistributionTime).sub(position.start); - time.increase(50); + await time.increase(50); // top-up await vestManager.topUpVestedDelegatePosition(delegatedValidator.address, { value: this.minDelegation }); @@ -1033,7 +1018,8 @@ export function RunVestedDelegateClaimTests(): void { ); }); - it("should claim rewards multiple times", async function () { + // TODO: Delete when remove the top-up functionality + it.skip("should claim rewards multiple times", async function () { const { systemValidatorSet, validatorSet, rewardPool, vestManager, vestManagerOwner, delegatedValidator } = await loadFixture(this.fixtures.weeklyVestedDelegationFixture); @@ -1051,23 +1037,17 @@ export function RunVestedDelegateClaimTests(): void { const rewardDistributionTime = await time.latest(); let position = await rewardPool.delegationPositions(delegatedValidator.address, vestManager.address); const toBeMatured = hre.ethers.BigNumber.from(rewardDistributionTime).sub(position.start); - time.increase(50); + await time.increase(50); // top-up await vestManager.topUpVestedDelegatePosition(delegatedValidator.address, { value: this.minDelegation }); // more rewards to be distributed but with the top-up data - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize - ); - // commit epoch, so more reward is added that must be without bonus - await commitEpoch( + await commitEpochs( systemValidatorSet, rewardPool, [this.signers.validators[0], this.signers.validators[1], delegatedValidator], + 2, this.epochSize ); @@ -1091,23 +1071,22 @@ export function RunVestedDelegateClaimTests(): void { "claimVestedPositionReward" ).to.changeEtherBalances([vestManagerOwner.address, rewardPool.address], [reward, maxBaseReward.mul(-1)]); - time.increase(WEEK * 2); - await commitEpoch( systemValidatorSet, rewardPool, [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize + this.epochSize, + WEEK * 2 ); expect(await vestManager.claimVestedPositionReward(delegatedValidator.address, epochNum + 1, topUpIndex + 1)).to .not.be.reverted; - time.increase(WEEK * 52); await commitEpoch( systemValidatorSet, rewardPool, [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize + this.epochSize, + WEEK * 52 ); expect(await vestManager.claimVestedPositionReward(delegatedValidator.address, epochNum + 1, topUpIndex + 1)).to @@ -1117,156 +1096,292 @@ export function RunVestedDelegateClaimTests(): void { } export function RunVestedDelegationSwapTests(): void { - describe.only("Delegate position rewards", async function () { - it("should swap vested delegation", async function () { - const { validatorSet, systemValidatorSet, rewardPool, vestManager, vestManagerOwner, liquidToken } = - await loadFixture(this.fixtures.vestManagerFixture); + describe("Delegate position rewards", async function () { + it("should revert when not the vest manager owner", async function () { + const { vestManager, delegatedValidator } = await loadFixture(this.fixtures.weeklyVestedDelegationFixture); - const validator = this.signers.validators[2]; - const vestingDuration = 11; // 11 weeks - await vestManager.connect(vestManagerOwner).openVestedDelegatePosition(validator.address, vestingDuration, { - value: this.minDelegation.mul(2), - }); + await expect( + vestManager + .connect(this.signers.accounts[10]) + .swapVestedValidator(delegatedValidator.address, delegatedValidator.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); - // 5 weeks ahead - await time.increase(WEEK * 5); + it("should revert when the delegator has no active position for the old validator", async function () { + const { vestManager, vestManagerOwner, rewardPool } = await loadFixture(this.fixtures.vestManagerFixture); + + await expect( + vestManager + .connect(vestManagerOwner) + .swapVestedValidator(this.signers.validators[0].address, this.signers.validators[1].address) + ) + .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") + .withArgs("vesting", "OLD_POSITION_INACTIVE"); + }); + + it("should revert that the position is inactive", async function () { + const { systemValidatorSet, vestManager, delegatedValidator, rewardPool, vestManagerOwner } = await loadFixture( + this.fixtures.weeklyVestedDelegationFixture + ); - // commit epoch, so more reward is added that must not be claimed now await commitEpoch( systemValidatorSet, rewardPool, - [this.signers.validators[0], this.signers.validators[1], validator], - this.epochSize + [this.signers.validators[0], this.signers.validators[1], delegatedValidator], + this.epochSize, + // increase time to make the position maturing + WEEK ); + // ensure is not active position + expect(await rewardPool.isActiveDelegatePosition(delegatedValidator.address, vestManager.address), "isActive").be + .false; + + await expect( + vestManager + .connect(vestManagerOwner) + .swapVestedValidator(this.signers.validators[0].address, this.signers.validators[1].address) + ) + .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") + .withArgs("vesting", "OLD_POSITION_INACTIVE"); + }); + + it("should revert when we try to swap to validator with maturing position", async function () { + const { systemValidatorSet, vestManager, liquidToken, vestManagerOwner, rewardPool } = await loadFixture( + this.fixtures.vestManagerFixture + ); + + const oldValidator = this.signers.validators[0]; const newValidator = this.signers.validators[1]; - const amount = await rewardPool + await liquidToken.connect(vestManagerOwner).approve(vestManager.address, this.minDelegation); + await vestManager + .connect(vestManagerOwner) + .openVestedDelegatePosition(oldValidator.address, 2, { value: this.minDelegation }); + await vestManager .connect(vestManagerOwner) - .getBalanceForVestedPosition(validator.address, vestManager.address); + .openVestedDelegatePosition(newValidator.address, 1, { value: this.minDelegation }); + + // commit 8 epochs with 1 day increase before each, so, the first position gonna start maturing + await commitEpochs(systemValidatorSet, rewardPool, [oldValidator, newValidator], 8, this.epochSize, DAY); + + const newPosition = await rewardPool.delegationPositions(newValidator.address, vestManager.address); + expect(newPosition.end.add(newPosition.duration).gt(await time.latest()), "Not matured").to.be.true; + expect(newPosition.end.lt(await time.latest()), "isMaturing").to.be.true; + + await expect( + vestManager.connect(vestManagerOwner).swapVestedValidator(oldValidator.address, newValidator.address) + ) + .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") + .withArgs("vesting", "NEW_POSITION_MATURING"); + }); + + it("should revert when we try to swap to active position (balance > 0)", async function () { + const { vestManager, liquidToken, vestManagerOwner, rewardPool } = await loadFixture( + this.fixtures.vestManagerFixture + ); + + const oldValidator = this.signers.validators[0]; + const newValidator = this.signers.validators[1]; + await liquidToken.connect(vestManagerOwner).approve(vestManager.address, this.minDelegation); + await vestManager + .connect(vestManagerOwner) + .openVestedDelegatePosition(oldValidator.address, 1, { value: this.minDelegation }); + await vestManager + .connect(vestManagerOwner) + .openVestedDelegatePosition(newValidator.address, 1, { value: this.minDelegation }); + + await expect( + vestManager.connect(vestManagerOwner).swapVestedValidator(oldValidator.address, newValidator.address) + ) + .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") + .withArgs("vesting", "INVALID_NEW_POSITION"); + }); + + it("should transfer old position parameters to the new one on successful swap", async function () { + const { systemValidatorSet, rewardPool, vestManager, vestManagerOwner, liquidToken } = await loadFixture( + this.fixtures.vestManagerFixture + ); + + const validator = this.signers.validators[0]; + const newValidator = this.signers.validators[1]; + + const vestingDuration = 2; // 2 weeks + await vestManager.connect(vestManagerOwner).openVestedDelegatePosition(validator.address, vestingDuration, { + value: this.minDelegation.mul(2), + }); + + await commitEpoch(systemValidatorSet, rewardPool, [validator, newValidator], this.epochSize); + + const amount = await rewardPool.delegationOf(validator.address, vestManager.address); // give allowance & swap await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); await vestManager.connect(vestManagerOwner).swapVestedValidator(validator.address, newValidator.address); - // check rewards for new validator - const rewardsAfterSwap = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); - expect(rewardsAfterSwap).to.be.eq(0); + const oldPosition = await rewardPool.delegationPositions(validator.address, vestManager.address); + const newPosition = await rewardPool.delegationPositions(newValidator.address, vestManager.address); - // expect the rewards to be more than 0 for 1st validator - const rewardsAfterSwapOldValidator = await rewardPool.getRawDelegatorReward( - validator.address, - vestManager.address + // expect new position to be like the old position + expect(oldPosition.duration, "oldPosition.duration").to.be.eq(newPosition.duration); + expect(oldPosition.start, "oldPosition.start").to.be.eq(newPosition.start); + expect(oldPosition.end, "oldPosition.end").to.be.eq(newPosition.end); + expect(oldPosition.base, "oldPosition.base").to.be.eq(newPosition.base); + expect(oldPosition.vestBonus, "oldPosition.vestBonus").to.be.eq(newPosition.vestBonus); + expect(oldPosition.rsiBonus, "oldPosition.rsiBonus").to.be.eq(newPosition.rsiBonus); + }); + + it("should start earning rewards on new position after swap", async function () { + const { systemValidatorSet, rewardPool, vestManager, oldValidator, newValidator } = await loadFixture( + this.fixtures.swappedPositionFixture ); - expect(rewardsAfterSwapOldValidator).to.be.not.eq(0); - // 6 weeks ahead for position to start maturing - await time.increase(WEEK * 6); + await commitEpoch(systemValidatorSet, rewardPool, [oldValidator, newValidator], this.epochSize); - // commit epoch - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], validator], - this.epochSize - ); + const rewardsAfterSwap = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); + expect(rewardsAfterSwap, "rewardsAfterSwap").to.be.gt(0); + }); - // see if position is maturing - const position = await rewardPool.delegationPositions(validator.address, vestManager.address); - expect(position.end.lt(await time.latest()), "isMaturing").to.be.true; + it("should stop earning rewards on old position after swap", async function () { + const { systemValidatorSet, rewardPool, vestManager, oldValidator, newValidator, rewardsBeforeSwap } = + await loadFixture(this.fixtures.swappedPositionFixture); - // 5 weeks ahead for position to claim matured rewards for 1st validator - await time.increase(WEEK * 5); + await commitEpoch(systemValidatorSet, rewardPool, [oldValidator, newValidator], this.epochSize); - // commit epoch - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], validator], - this.epochSize - ); + const rewardsAfterSwap = await rewardPool.getRawDelegatorReward(oldValidator.address, vestManager.address); + expect(rewardsAfterSwap, "rewardsAfterSwap").to.be.eq(rewardsBeforeSwap).and.to.be.gt(0); + }); + + it("should revert when pass incorrect swap index", async function () { + const { systemValidatorSet, rewardPool, vestManager, vestManagerOwner, oldValidator, newValidator } = + await loadFixture(this.fixtures.swappedPositionFixture); + + // commit epochs and increase time to make the position matured & commit epochs + await commitEpochs(systemValidatorSet, rewardPool, [oldValidator, newValidator], 4, this.epochSize, WEEK); + + const rewardsBeforeClaim = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); + expect(rewardsBeforeClaim).to.be.gt(0); // prepare params for call const { epochNum, topUpIndex } = await retrieveRPSData( - validatorSet, + systemValidatorSet, rewardPool, - validator.address, + newValidator.address, vestManager.address ); - // expect the rewards to be more than 0 for 1st validator - const rewardsBeforeClaim = await rewardPool.getRawDelegatorReward(validator.address, vestManager.address); - expect(rewardsBeforeClaim).to.be.not.eq(0); + await expect( + vestManager.connect(vestManagerOwner).claimVestedPositionReward(newValidator.address, epochNum, topUpIndex + 1) + ) + .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") + .withArgs("vesting", "INVALID_TOP_UP_INDEX"); + }); - vestManager.connect(vestManagerOwner).claimVestedPositionReward(validator.address, epochNum, topUpIndex); + it("should claim all rewards from the new position after swap", async function () { + const { systemValidatorSet, rewardPool, vestManager, vestManagerOwner, oldValidator, newValidator } = + await loadFixture(this.fixtures.swappedPositionFixture); - // commit epoch, to distribute rewards - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], validator], - this.epochSize - ); - - // expect the rewards to be 0 for 1st validator - const rewardsAfterClaim = await rewardPool.getRawDelegatorReward(validator.address, vestManager.address); + // commit epochs and increase time to make the position matured & commit epochs + await commitEpochs(systemValidatorSet, rewardPool, [oldValidator, newValidator], 4, this.epochSize, WEEK); - // 6 weeks ahead for position to be fully matured - await time.increase(WEEK * 6); + const rewardsBeforeClaim = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); + expect(rewardsBeforeClaim).to.be.gt(0); - // commit epoch - await commitEpoch( + // prepare params for call + const { epochNum, topUpIndex } = await retrieveRPSData( systemValidatorSet, rewardPool, - [this.signers.validators[0], this.signers.validators[1], validator], - this.epochSize + newValidator.address, + vestManager.address ); - // see if position is matured - const newPosition = await rewardPool.delegationPositions(newValidator.address, vestManager.address); - expect(newPosition.end.add(newPosition.duration).lt(await time.latest()), "isMatured").to.be.true; - // expect new position to be like the old position - expect(position.duration).to.be.eq(newPosition.duration); - expect(position.start).to.be.eq(newPosition.start); - expect(position.end).to.be.eq(newPosition.end); - expect(position.base).to.be.eq(newPosition.base); - expect(position.vestBonus).to.be.eq(newPosition.vestBonus); - expect(position.rsiBonus).to.be.eq(newPosition.rsiBonus); + await vestManager.connect(vestManagerOwner).claimVestedPositionReward(newValidator.address, epochNum, topUpIndex); - // expect the rewards to be still 0 for 1st validator - const rewardsAfterMatured = await rewardPool.getRawDelegatorReward(validator.address, vestManager.address); - expect(rewardsAfterClaim).to.be.eq(rewardsAfterMatured).and.to.be.eq(0); + const rewardsAfterClaim = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); + expect(rewardsAfterClaim).to.be.eq(0); + }); + + it("should claim all rewards from the old position after swap when the reward is matured", async function () { + const { systemValidatorSet, rewardPool, vestManager, vestManagerOwner, oldValidator, newValidator } = + await loadFixture(this.fixtures.swappedPositionFixture); - // expect the new rewards for new validator to be more than rewards for 1st validator before claim - const rewardsAfterMaturedNew = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); - expect(rewardsAfterMaturedNew).to.be.gt(rewardsBeforeClaim).and.to.be.gt(0); + // commit epochs and increase time to make the position matured & commit epochs + await commitEpochs(systemValidatorSet, rewardPool, [oldValidator, newValidator], 3, this.epochSize, WEEK); - // balance before claim - const balanceBefore = await vestManagerOwner.getBalance(); + const rewardsBeforeClaim = await rewardPool.getRawDelegatorReward(oldValidator.address, vestManager.address); + expect(rewardsBeforeClaim, "rewardsBeforeClaim").to.be.gt(0); // prepare params for call - const { epochNum: epochNumNew, topUpIndex: topUpIndexNew } = await retrieveRPSData( - validatorSet, + const { epochNum, topUpIndex } = await retrieveRPSData( + systemValidatorSet, rewardPool, - newValidator.address, + oldValidator.address, vestManager.address ); - vestManager.connect(vestManagerOwner).claimVestedPositionReward(newValidator.address, epochNumNew, topUpIndexNew); - // commit epoch, so more reward is added that must not be claimed now - await commitEpoch( + await vestManager.connect(vestManagerOwner).claimVestedPositionReward(oldValidator.address, epochNum, topUpIndex); + + const rewardsAfterClaim = await rewardPool.getRawDelegatorReward(oldValidator.address, vestManager.address); + expect(rewardsAfterClaim, "rewardsAfterClaim").to.be.eq(0); + }); + + it("should revert when try to swap again during the same epoch", async function () { + const { + rewardPool, + vestManager, + vestManagerOwner, + newValidator: oldValidator, + liquidToken, + } = await loadFixture(this.fixtures.swappedPositionFixture); + + const newValidator = this.signers.validators[2]; + const amount = await rewardPool.delegationOf(oldValidator.address, vestManager.address); + + // give allowance + await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); + + // try to swap + await expect( + vestManager.connect(vestManagerOwner).swapVestedValidator(oldValidator.address, newValidator.address) + ) + .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") + .withArgs("_onAccountParamsChange", "BALANCE_CHANGE_ALREADY_MADE"); + }); + + it("should revert when try to swap too many times", async function () { + const { systemValidatorSet, rewardPool, - [this.signers.validators[0], this.signers.validators[1], validator], - this.epochSize - ); + vestManager, + vestManagerOwner, + newValidator: oldValidator, + liquidToken, + } = await loadFixture(this.fixtures.swappedPositionFixture); + + const newValidator = this.signers.validators[2]; + + await commitEpoch(systemValidatorSet, rewardPool, [oldValidator, newValidator], this.epochSize, 60 * 60); + + const amount = await rewardPool.delegationOf(oldValidator.address, vestManager.address); - // expect the rewards to be 0 for new validator after claim - const rewardsAfterClaimNew = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); - expect(rewardsAfterClaimNew).to.be.eq(0); + const balanceChangesThreshold = (await rewardPool.balanceChangeThreshold()).toNumber(); + for (let i = 0; i < balanceChangesThreshold; i++) { + const _oldValidator = i % 2 === 0 ? oldValidator : newValidator; + const _newValidator = i % 2 === 0 ? newValidator : oldValidator; - // check balance after claim - const balanceAfter = await vestManagerOwner.getBalance(); - expect(balanceAfter).to.be.gt(balanceBefore); + // give allowance + await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); + await vestManager.connect(vestManagerOwner).swapVestedValidator(_oldValidator.address, _newValidator.address); + await commitEpoch(systemValidatorSet, rewardPool, [oldValidator, newValidator], this.epochSize, 60 * 60); + } + + await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); + + // try to swap to exceed the number of the allowed balance changes + await expect( + vestManager.connect(vestManagerOwner).swapVestedValidator(oldValidator.address, newValidator.address) + ) + .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") + .withArgs("_onAccountParamsChange", "BALANCE_CHANGES_EXCEEDED"); }); }); } diff --git a/test/ValidatorSet/Delegation.test.ts b/test/ValidatorSet/Delegation.test.ts index feb0cb2c..f103fcbe 100644 --- a/test/ValidatorSet/Delegation.test.ts +++ b/test/ValidatorSet/Delegation.test.ts @@ -10,9 +10,9 @@ import { calculatePenalty, claimPositionRewards, commitEpoch, commitEpochs, getU import { RunDelegateClaimTests, RunVestedDelegateClaimTests, - RunVestedDelegationSwapTests, RunVestedDelegationRewardsTests, RunDelegateFunctionsByValidatorSet, + RunVestedDelegationSwapTests, } from "../RewardPool/RewardPool.test"; export function RunDelegationTests(): void { @@ -681,7 +681,8 @@ export function RunDelegationTests(): void { }); }); - describe("topUpVestedDelegatePosition()", async function () { + // TODO: Delete when remove the top-up functionality + describe.skip("topUpVestedDelegatePosition()", async function () { it("should revert when not owner of the vest manager", async function () { const { vestManager } = await loadFixture(this.fixtures.vestedDelegationFixture); @@ -739,13 +740,12 @@ export function RunDelegationTests(): void { await rewardPool.delegationPositions(this.delegatedValidators[0], vestManager.address) ).end; - // enter the active state - await time.increase(1); await commitEpoch( systemValidatorSet, rewardPool, [this.signers.validators[0], this.signers.validators[1], this.signers.validators[2]], - this.epochSize + this.epochSize, + 1 // increase with 1 second to enter the active state ); // ensure position is active @@ -803,7 +803,7 @@ export function RunDelegationTests(): void { vestManager.topUpVestedDelegatePosition(this.delegatedValidators[0], { value: this.minDelegation }) ) .to.be.revertedWithCustomError(validatorSet, "DelegateRequirement") - .withArgs("vesting", "TOO_MANY_TOP_UPS"); + .withArgs("vesting", "BALANCE_CHANGES_EXCEEDED"); }); it("should revert when top-up already made in the same epoch", async function () { diff --git a/test/constants.ts b/test/constants.ts index b7b610b8..ffe6ccd3 100644 --- a/test/constants.ts +++ b/test/constants.ts @@ -10,7 +10,8 @@ export const VALIDATOR_PKCHECK_PRECOMPILE_GAS = 150000; export const CHAIN_ID = 31337; export const INITIAL_COMMISSION = ethers.BigNumber.from(10); export const MAX_COMMISSION = ethers.BigNumber.from(100); -export const WEEK = 60 * 60 * 24 * 7; +export const DAY = 60 * 60 * 24; +export const WEEK = DAY * 7; export const VESTING_DURATION_WEEKS = 10; // in weeks export const EPOCHS_YEAR = ethers.BigNumber.from(31500); export const INITIAL_BASE_APR = ethers.BigNumber.from(500); diff --git a/test/fixtures.ts b/test/fixtures.ts index 727cd199..a9482c5f 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -19,6 +19,7 @@ import { registerValidator, createNewVestManager, generateValidatorBls, + commitEpoch, } from "./helper"; async function systemFixtureFunction(this: Mocha.Context) { @@ -461,6 +462,41 @@ async function bannedValidatorFixtureFunction(this: Mocha.Context) { }; } +async function swappedPositionFixtureFunction(this: Mocha.Context) { + const { validatorSet, systemValidatorSet, rewardPool, liquidToken, vestManager, vestManagerOwner } = + await loadFixture(this.fixtures.vestManagerFixture); + + const validator = this.signers.validators[0]; + const newValidator = this.signers.validators[1]; + + const vestingDuration = 2; // 2 weeks + await vestManager.connect(vestManagerOwner).openVestedDelegatePosition(validator.address, vestingDuration, { + value: this.minDelegation.mul(2), + }); + + await commitEpoch(systemValidatorSet, rewardPool, [validator, newValidator], this.epochSize); + + const rewardsBeforeSwap = await rewardPool.getRawDelegatorReward(validator.address, vestManager.address); + + const amount = await rewardPool.delegationOf(validator.address, vestManager.address); + + // give allowance & swap + await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); + await vestManager.connect(vestManagerOwner).swapVestedValidator(validator.address, newValidator.address); + + return { + validatorSet, + systemValidatorSet, + rewardPool, + liquidToken, + vestManager, + vestManagerOwner, + oldValidator: validator, + newValidator, + rewardsBeforeSwap, + }; +} + async function blsFixtureFunction(this: Mocha.Context) { const BLSFactory = new BLS__factory(this.signers.admin); const BLS = await BLSFactory.deploy(); @@ -499,4 +535,5 @@ export async function generateFixtures(context: Mocha.Context) { context.fixtures.weeklyVestedDelegationFixture = weeklyVestedDelegationFixtureFunction.bind(context); context.fixtures.validatorToBanFixture = validatorToBanFixtureFunction.bind(context); context.fixtures.bannedValidatorFixture = bannedValidatorFixtureFunction.bind(context); + context.fixtures.swappedPositionFixture = swappedPositionFixtureFunction.bind(context); } diff --git a/test/helper.ts b/test/helper.ts index 9661370a..6979d6a2 100644 --- a/test/helper.ts +++ b/test/helper.ts @@ -1,7 +1,7 @@ /* eslint-disable camelcase */ /* eslint-disable node/no-extraneous-import */ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; -import { mine } from "@nomicfoundation/hardhat-network-helpers"; +import { mine, time } from "@nomicfoundation/hardhat-network-helpers"; import * as hre from "hardhat"; import { BigNumber, ContractTransaction } from "ethers"; @@ -11,7 +11,7 @@ import { ValidatorSet } from "../typechain-types/contracts/ValidatorSet"; import { RewardPool } from "../typechain-types/contracts/RewardPool"; import { VestManager } from "../typechain-types/contracts/ValidatorSet/modules/Delegation"; import { VestManager__factory } from "../typechain-types/factories/contracts/ValidatorSet/modules/Delegation"; -import { CHAIN_ID, DENOMINATOR, DOMAIN, EPOCHS_YEAR, INITIAL_COMMISSION, SYSTEM, WEEK } from "./constants"; +import { CHAIN_ID, DAY, DENOMINATOR, DOMAIN, EPOCHS_YEAR, INITIAL_COMMISSION, SYSTEM, WEEK } from "./constants"; interface RewardParams { timestamp: BigNumber; @@ -61,7 +61,8 @@ export async function commitEpoch( systemValidatorSet: ValidatorSet, rewardPool: RewardPool, validators: SignerWithAddress[], - epochSize: BigNumber + epochSize: BigNumber, + increaseTime?: number ): Promise<{ commitEpochTx: ContractTransaction; distributeRewardsTx: ContractTransaction }> { const currEpochId = await systemValidatorSet.currentEpochId(); const prevEpochId = currEpochId.sub(1); @@ -78,6 +79,8 @@ export async function commitEpoch( } await mine(epochSize, { interval: 2 }); + increaseTime = increaseTime || DAY; // default 1 day + await time.increase(increaseTime); const commitEpochTx = await systemValidatorSet.commitEpoch(currEpochId, newEpoch, epochSize); @@ -96,12 +99,13 @@ export async function commitEpochs( rewardPool: RewardPool, validators: SignerWithAddress[], numOfEpochsToCommit: number, - epochSize: BigNumber + epochSize: BigNumber, + increaseTime?: number ) { if (epochSize.isZero() || numOfEpochsToCommit === 0) return; for (let i = 0; i < numOfEpochsToCommit; i++) { - await commitEpoch(systemValidatorSet, rewardPool, validators, epochSize); + await commitEpoch(systemValidatorSet, rewardPool, validators, epochSize, increaseTime); } } @@ -170,6 +174,39 @@ export function findProperRPSIndex(arr: T[], timestamp: return closestIndex; } +export function findTopUpIndex(arr: any[], epochNum: BigNumber): number { + if (arr.length <= 1) return 0; + + let left = 0; + let right = arr.length - 1; + let closestEpoch: null | BigNumber = null; + let closestIndex: null | number = null; + + while (left <= right) { + const mid = Math.floor((left + right) / 2); + const midValue = arr[mid].epochNum; + if (midValue.eq(epochNum)) { + // Timestamp found + return mid; + } else if (midValue.lt(epochNum)) { + // Check if the timestamp is closer to the mid + if (closestEpoch === null || epochNum.sub(midValue).abs().lt(epochNum.sub(closestEpoch).abs())) { + closestEpoch = midValue; + closestIndex = mid; + } + left = mid + 1; + } else { + right = mid - 1; + } + } + + if (closestIndex === null) { + throw new Error("findTopUpIndex: Invalid epoch number"); + } + + return closestIndex; +} + export async function calculatePenalty(position: any, timestamp: BigNumber, amount: BigNumber) { const leftPeriod: BigNumber = position.end.sub(timestamp); let leftWeeks = leftPeriod.mod(WEEK); // get the remainder first @@ -235,11 +272,12 @@ export async function retrieveRPSData( manager: string ) { const position = await rewardPool.delegationPositions(validator, manager); - const end = position.end; + const maturedIn = await getClosestMaturedTimestamp(position); const currentEpochId = await validatorSet.currentEpochId(); const rpsValues = await rewardPool.getRPSValues(validator, 0, currentEpochId); - const epochNum = findProperRPSIndex(rpsValues, end); - const topUpIndex = 0; + const epochNum = findProperRPSIndex(rpsValues, hre.ethers.BigNumber.from(maturedIn)); + const delegationPoolParamsHistory = await rewardPool.getDelegationPoolParamsHistory(validator, manager); + const topUpIndex = findTopUpIndex(delegationPoolParamsHistory, hre.ethers.BigNumber.from(epochNum)); return { position, epochNum, topUpIndex }; } @@ -331,3 +369,23 @@ export async function getDelegatorPositionReward( return await rewardPool.getDelegatorPositionReward(validator, delegator, epochNum, topUpIndex); } + +export async function getClosestMaturedTimestamp(position: any) { + let alreadyMatureIn = 0; + if (await hasMatured(position.end, position.duration)) { + alreadyMatureIn = position.end; + } else { + const currChainTs = await time.latest(); + const maturedPeriod = currChainTs - position.end; + alreadyMatureIn = position.start.add(maturedPeriod); + } + + return alreadyMatureIn; +} + +// function that returns whether a position is matured or not +async function hasMatured(positionEnd: BigNumber, positionDuration: BigNumber) { + const currChainTs = await time.latest(); + + return positionEnd && positionDuration && positionEnd.add(positionDuration).lte(currChainTs); +} diff --git a/test/mochaContext.ts b/test/mochaContext.ts index fb82d907..9b8df4c0 100644 --- a/test/mochaContext.ts +++ b/test/mochaContext.ts @@ -169,6 +169,19 @@ export interface Fixtures { stakedAmount: BigNumber; }>; }; + swappedPositionFixture: { + (): Promise<{ + validatorSet: ValidatorSet; + systemValidatorSet: ValidatorSet; + rewardPool: RewardPool; + liquidToken: LiquidityToken; + vestManager: VestManager; + vestManagerOwner: SignerWithAddress; + oldValidator: SignerWithAddress; + newValidator: SignerWithAddress; + rewardsBeforeSwap: BigNumber; + }>; + }; } declare module "mocha" { From 2cfa60eb7a45fbfec51a9eb28254978ff1bf4ce6 Mon Sep 17 00:00:00 2001 From: Vitomir Pavlov Date: Wed, 5 Jun 2024 16:02:34 +0300 Subject: [PATCH 4/7] clean npx cache and delete console.md --- docs/console.md | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 docs/console.md diff --git a/docs/console.md b/docs/console.md deleted file mode 100644 index 8bab67a4..00000000 --- a/docs/console.md +++ /dev/null @@ -1,12 +0,0 @@ -# console - - - - - - - - - - - From 61010a4554ddb89aab65c42380e7d8dca51d2e49 Mon Sep 17 00:00:00 2001 From: Rosen Santev Date: Thu, 6 Jun 2024 13:51:47 +0300 Subject: [PATCH 5/7] Polishing --- contracts/RewardPool/IRewardPool.sol | 2 +- .../RewardPool/modules/DelegationRewards.sol | 62 ++- .../modules/Delegation/Delegation.sol | 10 +- .../modules/Delegation/IDelegation.sol | 4 +- .../modules/Delegation/VestManager.sol | 7 +- docs/RewardPool/IRewardPool.md | 2 +- docs/RewardPool/RewardPool.md | 2 +- docs/RewardPool/RewardPoolBase.md | 2 +- docs/RewardPool/modules/DelegationRewards.md | 2 +- docs/RewardPool/modules/StakingRewards.md | 2 +- docs/ValidatorSet/ValidatorSet.md | 6 +- .../modules/Delegation/Delegation.md | 6 +- .../modules/Delegation/IDelegation.md | 6 +- .../modules/Delegation/VestManager.md | 4 +- .../modules/PowerExponent/PowerExponent.md | 467 ------------------ test/RewardPool/RewardPool.test.ts | 291 ----------- test/ValidatorSet/Delegation.test.ts | 4 +- .../SwapVestedPositionValidator.test.ts | 287 +++++++++++ test/fixtures.ts | 2 +- 19 files changed, 345 insertions(+), 823 deletions(-) create mode 100644 test/ValidatorSet/SwapVestedPositionValidator.test.ts diff --git a/contracts/RewardPool/IRewardPool.sol b/contracts/RewardPool/IRewardPool.sol index 575f2233..69cc7f7e 100644 --- a/contracts/RewardPool/IRewardPool.sol +++ b/contracts/RewardPool/IRewardPool.sol @@ -273,7 +273,7 @@ interface IRewardPool { function changeMinDelegation(uint256 newMinDelegation) external; /** - * @notice Changes the threshold for the balance change + * @notice Modifies the balance changes threshold for vested positions * @dev Should be called only by the Governance. * @param newBalanceChangeThreshold The number of allowed changes of the balance */ diff --git a/contracts/RewardPool/modules/DelegationRewards.sol b/contracts/RewardPool/modules/DelegationRewards.sol index 1eb48304..45678b13 100644 --- a/contracts/RewardPool/modules/DelegationRewards.sol +++ b/contracts/RewardPool/modules/DelegationRewards.sol @@ -226,7 +226,7 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa }); // keep the change in the delegation pool params per account - _addNewDelegationPoolParam( + _saveAccountParamsChange( validator, delegator, DelegationPoolParams({ @@ -291,9 +291,15 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa delete delegationPoolParamsHistory[validator][delegator]; } else { // keep the change in the account pool params - uint256 balance = delegation.balanceOf(delegator); - int256 correction = delegation.correctionOf(delegator); - _onAccountParamsChange(validator, delegator, balance, correction, currentEpochId); + _saveAccountParamsChange( + validator, + delegator, + DelegationPoolParams({ + balance: delegation.balanceOf(delegator), + correction: delegation.correctionOf(delegator), + epochNum: currentEpochId + }) + ); } } @@ -334,7 +340,11 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa oldDelegation.withdraw(delegator, amount); int256 correction = oldDelegation.correctionOf(delegator); - _onAccountParamsChange(oldValidator, delegator, 0, correction, currentEpochId); + _saveAccountParamsChange( + oldValidator, + delegator, + DelegationPoolParams({balance: 0, correction: correction, epochNum: currentEpochId}) + ); // set the new delegation position using the old position parameters newDelegation.deposit(delegator, amount); @@ -349,7 +359,7 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa }); // keep the change in the new delegation pool params - _addNewDelegationPoolParam( + _saveAccountParamsChange( newValidator, delegator, DelegationPoolParams({ @@ -559,9 +569,15 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa // keep the change in the account pool params uint256 balance = delegation.balanceOf(delegator); - int256 correction = delegation.correctionOf(delegator); - - _onAccountParamsChange(validator, delegator, balance, correction, currentEpochId); + _saveAccountParamsChange( + validator, + delegator, + DelegationPoolParams({ + balance: balance, + correction: delegation.correctionOf(delegator), + epochNum: currentEpochId + }) + ); // Modify end period of position, decrease RSI bonus // balance / old balance = increase coefficient // apply increase coefficient to the vesting period to find the increase in the period @@ -590,19 +606,19 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa _withdrawRewards(delegator, reward); } - function _addNewDelegationPoolParam( + function _saveAccountParamsChange( address validator, address delegator, DelegationPoolParams memory params ) private { if (isBalanceChangeMade(validator, delegator, params.epochNum)) { // balance can be changed only once per epoch - revert DelegateRequirement({src: "_addNewDelegationPoolParam", msg: "BALANCE_CHANGE_ALREADY_MADE"}); + revert DelegateRequirement({src: "_saveAccountParamsChange", msg: "BALANCE_CHANGE_ALREADY_MADE"}); } if (isBalanceChangeThresholdExceeded(validator, delegator)) { // maximum amount of balance changes exceeded - revert DelegateRequirement({src: "_addNewDelegationPoolParam", msg: "BALANCE_CHANGES_EXCEEDED"}); + revert DelegateRequirement({src: "_saveAccountParamsChange", msg: "BALANCE_CHANGES_EXCEEDED"}); } delegationPoolParamsHistory[validator][delegator].push(params); @@ -623,28 +639,6 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa }); } - function _onAccountParamsChange( - address validator, - address delegator, - uint256 balance, - int256 correction, - uint256 currentEpochId - ) private { - if (isBalanceChangeMade(validator, delegator, currentEpochId)) { - // balance can be changed only once per epoch - revert DelegateRequirement({src: "_onAccountParamsChange", msg: "BALANCE_CHANGE_ALREADY_MADE"}); - } - - if (isBalanceChangeThresholdExceeded(validator, delegator)) { - // maximum amount of balance changes exceeded - revert DelegateRequirement({src: "_onAccountParamsChange", msg: "BALANCE_CHANGES_EXCEEDED"}); - } - - delegationPoolParamsHistory[validator][delegator].push( - DelegationPoolParams({balance: balance, correction: correction, epochNum: currentEpochId}) - ); - } - function _getAccountParams( address validator, address manager, diff --git a/contracts/ValidatorSet/modules/Delegation/Delegation.sol b/contracts/ValidatorSet/modules/Delegation/Delegation.sol index 16d0fc3b..0bb5a224 100644 --- a/contracts/ValidatorSet/modules/Delegation/Delegation.sol +++ b/contracts/ValidatorSet/modules/Delegation/Delegation.sol @@ -33,6 +33,7 @@ abstract contract Delegation is function delegate(address validator) public payable { if (msg.value == 0) revert DelegateRequirement({src: "delegate", msg: "DELEGATING_AMOUNT_ZERO"}); _delegate(validator, msg.sender, msg.value); + LiquidStaking._distributeTokens(msg.sender, msg.value); rewardPool.onDelegate(validator, msg.sender, msg.value); } @@ -42,6 +43,7 @@ abstract contract Delegation is function undelegate(address validator, uint256 amount) external { rewardPool.onUndelegate(validator, msg.sender, amount); _undelegate(validator, msg.sender, amount); + LiquidStaking._collectDelegatorTokens(msg.sender, amount); _registerWithdrawal(msg.sender, amount); } @@ -50,6 +52,7 @@ abstract contract Delegation is */ function delegateWithVesting(address validator, uint256 durationWeeks) external payable onlyManager { _delegate(validator, msg.sender, msg.value); + LiquidStaking._distributeTokens(msg.sender, msg.value); rewardPool.onNewDelegatePosition(validator, msg.sender, durationWeeks, currentEpochId, msg.value); emit PositionOpened(msg.sender, validator, durationWeeks, msg.value); @@ -60,7 +63,7 @@ abstract contract Delegation is */ function topUpDelegatePosition(address validator) external payable onlyManager { _delegate(validator, msg.sender, msg.value); - + LiquidStaking._distributeTokens(msg.sender, msg.value); rewardPool.onTopUpDelegatePosition(validator, msg.sender, currentEpochId, msg.value); emit PositionTopUp(msg.sender, validator, msg.value); @@ -72,6 +75,7 @@ abstract contract Delegation is function undelegateWithVesting(address validator, uint256 amount) external onlyManager { (uint256 penalty, ) = rewardPool.onCutPosition(validator, msg.sender, amount, currentEpochId); _undelegate(validator, msg.sender, amount); + LiquidStaking._collectDelegatorTokens(msg.sender, amount); uint256 amountAfterPenalty = amount - penalty; _burnAmount(penalty); _registerWithdrawal(msg.sender, amountAfterPenalty); @@ -82,7 +86,7 @@ abstract contract Delegation is /** * @inheritdoc IDelegation */ - function swapVestedValidator(address oldValidator, address newValidator) external onlyManager { + function swapVestedPositionValidator(address oldValidator, address newValidator) external onlyManager { uint256 amount = rewardPool.onSwapPosition(oldValidator, newValidator, msg.sender, currentEpochId); _undelegate(oldValidator, msg.sender, amount); _delegate(newValidator, msg.sender, amount); @@ -95,7 +99,6 @@ abstract contract Delegation is function _delegate(address validator, address delegator, uint256 amount) private onlyActiveValidator(validator) { _increaseAccountBalance(validator, amount); // increase validator power StateSyncer._syncStake(validator, balanceOf(validator)); - LiquidStaking._distributeTokens(delegator, amount); emit Delegated(validator, delegator, amount); } @@ -103,7 +106,6 @@ abstract contract Delegation is function _undelegate(address validator, address delegator, uint256 amount) private { _decreaseAccountBalance(validator, amount); // decrease validator power StateSyncer._syncStake(validator, balanceOf(validator)); - LiquidStaking._collectDelegatorTokens(delegator, amount); emit Undelegated(validator, delegator, amount); } diff --git a/contracts/ValidatorSet/modules/Delegation/IDelegation.sol b/contracts/ValidatorSet/modules/Delegation/IDelegation.sol index 7370e507..20e6b1be 100644 --- a/contracts/ValidatorSet/modules/Delegation/IDelegation.sol +++ b/contracts/ValidatorSet/modules/Delegation/IDelegation.sol @@ -45,10 +45,10 @@ interface IDelegation { function undelegateWithVesting(address validator, uint256 amount) external; /** - * @notice Swaps the validator for a vesting position. + * @notice Move a vested position to another validator. * Can be called by vesting positions' managers only. * @param oldValidator Validator to swap from * @param newValidator Validator to swap to */ - function swapVestedValidator(address oldValidator, address newValidator) external; + function swapVestedPositionValidator(address oldValidator, address newValidator) external; } diff --git a/contracts/ValidatorSet/modules/Delegation/VestManager.sol b/contracts/ValidatorSet/modules/Delegation/VestManager.sol index cb589798..b2ab7a84 100644 --- a/contracts/ValidatorSet/modules/Delegation/VestManager.sol +++ b/contracts/ValidatorSet/modules/Delegation/VestManager.sol @@ -53,11 +53,8 @@ contract VestManager is Initializable, OwnableUpgradeable { IDelegation(delegation).undelegateWithVesting(validator, amount); } - function swapVestedValidator(address oldValidator, address newValidator) external onlyOwner { - uint256 amount = IRewardPool(rewardPool).delegationOf(oldValidator, address(this)); - _fulfillLiquidTokens(msg.sender, amount); - IDelegation(delegation).swapVestedValidator(oldValidator, newValidator); - _sendLiquidTokens(msg.sender, amount); + function swapVestedPositionValidator(address oldValidator, address newValidator) external onlyOwner { + IDelegation(delegation).swapVestedPositionValidator(oldValidator, newValidator); } function claimVestedPositionReward( diff --git a/docs/RewardPool/IRewardPool.md b/docs/RewardPool/IRewardPool.md index afc0901b..a4b042d6 100644 --- a/docs/RewardPool/IRewardPool.md +++ b/docs/RewardPool/IRewardPool.md @@ -87,7 +87,7 @@ Returns the total reward that is generated for a position function changeBalanceChangeThreshold(uint256 newBalanceChangeThreshold) external nonpayable ``` -Changes the threshold for the balance change +Modifies the balance changes threshold for vested positions *Should be called only by the Governance.* diff --git a/docs/RewardPool/RewardPool.md b/docs/RewardPool/RewardPool.md index 2003fc50..39ce12f8 100644 --- a/docs/RewardPool/RewardPool.md +++ b/docs/RewardPool/RewardPool.md @@ -423,7 +423,7 @@ Returns the total reward that is generated for a position function changeBalanceChangeThreshold(uint256 newBalanceChangeThreshold) external nonpayable ``` -Changes the threshold for the balance change +Modifies the balance changes threshold for vested positions *Should be called only by the Governance.* diff --git a/docs/RewardPool/RewardPoolBase.md b/docs/RewardPool/RewardPoolBase.md index 8d1299f3..03d65cac 100644 --- a/docs/RewardPool/RewardPoolBase.md +++ b/docs/RewardPool/RewardPoolBase.md @@ -87,7 +87,7 @@ Returns the total reward that is generated for a position function changeBalanceChangeThreshold(uint256 newBalanceChangeThreshold) external nonpayable ``` -Changes the threshold for the balance change +Modifies the balance changes threshold for vested positions *Should be called only by the Governance.* diff --git a/docs/RewardPool/modules/DelegationRewards.md b/docs/RewardPool/modules/DelegationRewards.md index e3da5c64..0a2d6045 100644 --- a/docs/RewardPool/modules/DelegationRewards.md +++ b/docs/RewardPool/modules/DelegationRewards.md @@ -321,7 +321,7 @@ Returns the total reward that is generated for a position function changeBalanceChangeThreshold(uint256 newBalanceChangeThreshold) external nonpayable ``` -Changes the threshold for the balance change +Modifies the balance changes threshold for vested positions *Should be called only by the Governance.* diff --git a/docs/RewardPool/modules/StakingRewards.md b/docs/RewardPool/modules/StakingRewards.md index e54accec..a5f2d46e 100644 --- a/docs/RewardPool/modules/StakingRewards.md +++ b/docs/RewardPool/modules/StakingRewards.md @@ -304,7 +304,7 @@ Returns the total reward that is generated for a position function changeBalanceChangeThreshold(uint256 newBalanceChangeThreshold) external nonpayable ``` -Changes the threshold for the balance change +Modifies the balance changes threshold for vested positions *Should be called only by the Governance.* diff --git a/docs/ValidatorSet/ValidatorSet.md b/docs/ValidatorSet/ValidatorSet.md index c63ca56a..6cc51074 100644 --- a/docs/ValidatorSet/ValidatorSet.md +++ b/docs/ValidatorSet/ValidatorSet.md @@ -959,13 +959,13 @@ Stakes sent amount with vesting period. |---|---|---| | durationWeeks | uint256 | Duration of the vesting in weeks. Must be between 1 and 52. | -### swapVestedValidator +### swapVestedPositionValidator ```solidity -function swapVestedValidator(address oldValidator, address newValidator) external nonpayable +function swapVestedPositionValidator(address oldValidator, address newValidator) external nonpayable ``` -Swaps the validator for a vesting position. Can be called by vesting positions' managers only. +Move a vested position to another validator. Can be called by vesting positions' managers only. diff --git a/docs/ValidatorSet/modules/Delegation/Delegation.md b/docs/ValidatorSet/modules/Delegation/Delegation.md index 3a947659..f0b56e95 100644 --- a/docs/ValidatorSet/modules/Delegation/Delegation.md +++ b/docs/ValidatorSet/modules/Delegation/Delegation.md @@ -538,13 +538,13 @@ function stakeBalances(address) external view returns (uint256) |---|---|---| | _0 | uint256 | undefined | -### swapVestedValidator +### swapVestedPositionValidator ```solidity -function swapVestedValidator(address oldValidator, address newValidator) external nonpayable +function swapVestedPositionValidator(address oldValidator, address newValidator) external nonpayable ``` -Swaps the validator for a vesting position. Can be called by vesting positions' managers only. +Move a vested position to another validator. Can be called by vesting positions' managers only. diff --git a/docs/ValidatorSet/modules/Delegation/IDelegation.md b/docs/ValidatorSet/modules/Delegation/IDelegation.md index 0a5e6460..6f77865d 100644 --- a/docs/ValidatorSet/modules/Delegation/IDelegation.md +++ b/docs/ValidatorSet/modules/Delegation/IDelegation.md @@ -43,13 +43,13 @@ Delegates sent amount to validator. Set vesting position data. Delete old top-up | validator | address | Validator to delegate to | | durationWeeks | uint256 | Duration of the vesting in weeks | -### swapVestedValidator +### swapVestedPositionValidator ```solidity -function swapVestedValidator(address oldValidator, address newValidator) external nonpayable +function swapVestedPositionValidator(address oldValidator, address newValidator) external nonpayable ``` -Swaps the validator for a vesting position. Can be called by vesting positions' managers only. +Move a vested position to another validator. Can be called by vesting positions' managers only. diff --git a/docs/ValidatorSet/modules/Delegation/VestManager.md b/docs/ValidatorSet/modules/Delegation/VestManager.md index 08b8458c..fe2a22f8 100644 --- a/docs/ValidatorSet/modules/Delegation/VestManager.md +++ b/docs/ValidatorSet/modules/Delegation/VestManager.md @@ -141,10 +141,10 @@ The reward pool address |---|---|---| | _0 | address | undefined | -### swapVestedValidator +### swapVestedPositionValidator ```solidity -function swapVestedValidator(address oldValidator, address newValidator) external nonpayable +function swapVestedPositionValidator(address oldValidator, address newValidator) external nonpayable ``` diff --git a/docs/ValidatorSet/modules/PowerExponent/PowerExponent.md b/docs/ValidatorSet/modules/PowerExponent/PowerExponent.md index e37ccb24..a05a85de 100644 --- a/docs/ValidatorSet/modules/PowerExponent/PowerExponent.md +++ b/docs/ValidatorSet/modules/PowerExponent/PowerExponent.md @@ -10,23 +10,6 @@ ## Methods -### DOMAIN - -```solidity -function DOMAIN() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined | - ### acceptOwnership ```solidity @@ -38,180 +21,6 @@ function acceptOwnership() external nonpayable *The new owner accepts the ownership transfer.* -### activeValidatorsCount - -```solidity -function activeValidatorsCount() external view returns (uint256) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - -### addToWhitelist - -```solidity -function addToWhitelist(address[] whitelistAddreses) external nonpayable -``` - -Adds addresses that are allowed to register as validators. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| whitelistAddreses | address[] | Array of address to whitelist | - -### balanceOf - -```solidity -function balanceOf(address account) external view returns (uint256) -``` - -Returns the total balance of a given validator - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| account | address | The address of the validator | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | Validator's balance | - -### bls - -```solidity -function bls() external view returns (contract IBLS) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | contract IBLS | undefined | - -### currentEpochId - -```solidity -function currentEpochId() external view returns (uint256) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - -### epochEndBlocks - -```solidity -function epochEndBlocks(uint256) external view returns (uint256) -``` - -Array with epoch ending blocks - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - -### epochs - -```solidity -function epochs(uint256) external view returns (uint256 startBlock, uint256 endBlock, bytes32 epochRoot) -``` - -Epoch data linked with the epoch id - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| startBlock | uint256 | undefined | -| endBlock | uint256 | undefined | -| epochRoot | bytes32 | undefined | - -### getActiveValidatorsCount - -```solidity -function getActiveValidatorsCount() external view returns (uint256) -``` - -Gets the number of current validators - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | Returns the count as uint256 | - -### getEpochByBlock - -```solidity -function getEpochByBlock(uint256 blockNumber) external view returns (struct Epoch) -``` - -Look up an epoch by block number. Searches in O(log n) time. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| blockNumber | uint256 | ID of epoch to be committed | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | Epoch | Epoch Returns epoch if found, or else, the last epoch | - ### getExponent ```solidity @@ -230,50 +39,6 @@ Return the Voting Power Exponent Numerator and Denominator | numerator | uint256 | undefined | | denominator | uint256 | undefined | -### getValidator - -```solidity -function getValidator(address validator) external view returns (uint256[4] blsKey, uint256 stake, uint256 totalStake, uint256 commission, uint256 withdrawableRewards, bool active) -``` - -Gets validator by address. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| validator | address | Address of the validator | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| blsKey | uint256[4] | BLS public key | -| stake | uint256 | self-stake | -| totalStake | uint256 | self-stake + delegation | -| commission | uint256 | commission | -| withdrawableRewards | uint256 | withdrawable rewards | -| active | bool | activity status | - -### getValidators - -```solidity -function getValidators() external view returns (address[]) -``` - -Gets all validators. Returns already unactive validators as well. - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address[] | Returns array of addresses | - ### owner ```solidity @@ -326,22 +91,6 @@ function powerExponent() external view returns (uint128 value, uint128 pendingVa | value | uint128 | undefined | | pendingValue | uint128 | undefined | -### removeFromWhitelist - -```solidity -function removeFromWhitelist(address[] whitelistAddreses) external nonpayable -``` - -Deletes addresses that are allowed to register as validators. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| whitelistAddreses | address[] | Array of address to remove from whitelist | - ### renounceOwnership ```solidity @@ -353,62 +102,6 @@ function renounceOwnership() external nonpayable *Leaves the contract without owner. It will not be possible to call `onlyOwner` functions. Can only be called by the current owner. NOTE: Renouncing ownership will leave the contract without an owner, thereby disabling any functionality that is only available to the owner.* -### rewardPool - -```solidity -function rewardPool() external view returns (contract IRewardPool) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | contract IRewardPool | undefined | - -### totalBlocks - -```solidity -function totalBlocks(uint256 epochId) external view returns (uint256 length) -``` - -Total amount of blocks in a given epoch - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| epochId | uint256 | The number of the epoch | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| length | uint256 | Total blocks for an epoch | - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256) -``` - -Returns the total supply - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | Total supply | - ### transferOwnership ```solidity @@ -441,110 +134,10 @@ Set new pending exponent, to be activated in the next commit epoch |---|---|---| | newValue | uint256 | New Voting Power Exponent Numerator | -### updateValidatorParticipation - -```solidity -function updateValidatorParticipation(address validator) external nonpayable -``` - -Method to update when the validator was lastly active which can be executed only by the RewardPool - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| validator | address | The validator to set the last participation for | - -### validatorParticipation - -```solidity -function validatorParticipation(address) external view returns (uint256) -``` - -Mapping that keeps the last time when a validator has participated in the consensus - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - -### validators - -```solidity -function validators(address) external view returns (uint256 liquidDebt, uint256 commission, enum ValidatorStatus status) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| liquidDebt | uint256 | undefined | -| commission | uint256 | undefined | -| status | enum ValidatorStatus | undefined | - -### validatorsAddresses - -```solidity -function validatorsAddresses(uint256) external view returns (address) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined | - ## Events -### AddedToWhitelist - -```solidity -event AddedToWhitelist(address indexed validator) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| validator `indexed` | address | undefined | - ### Initialized ```solidity @@ -561,25 +154,6 @@ event Initialized(uint8 version) |---|---|---| | version | uint8 | undefined | -### NewEpoch - -```solidity -event NewEpoch(uint256 indexed id, uint256 indexed startBlock, uint256 indexed endBlock, bytes32 epochRoot) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| id `indexed` | uint256 | undefined | -| startBlock `indexed` | uint256 | undefined | -| endBlock `indexed` | uint256 | undefined | -| epochRoot | bytes32 | undefined | - ### OwnershipTransferStarted ```solidity @@ -614,46 +188,5 @@ event OwnershipTransferred(address indexed previousOwner, address indexed newOwn | previousOwner `indexed` | address | undefined | | newOwner `indexed` | address | undefined | -### RemovedFromWhitelist - -```solidity -event RemovedFromWhitelist(address indexed validator) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| validator `indexed` | address | undefined | - - - -## Errors - -### MustBeWhitelisted - -```solidity -error MustBeWhitelisted() -``` - - - - - - -### PreviouslyWhitelisted - -```solidity -error PreviouslyWhitelisted() -``` - - - - - diff --git a/test/RewardPool/RewardPool.test.ts b/test/RewardPool/RewardPool.test.ts index b464afc7..e51b0a9b 100644 --- a/test/RewardPool/RewardPool.test.ts +++ b/test/RewardPool/RewardPool.test.ts @@ -1094,294 +1094,3 @@ export function RunVestedDelegateClaimTests(): void { }); }); } - -export function RunVestedDelegationSwapTests(): void { - describe("Delegate position rewards", async function () { - it("should revert when not the vest manager owner", async function () { - const { vestManager, delegatedValidator } = await loadFixture(this.fixtures.weeklyVestedDelegationFixture); - - await expect( - vestManager - .connect(this.signers.accounts[10]) - .swapVestedValidator(delegatedValidator.address, delegatedValidator.address) - ).to.be.revertedWith("Ownable: caller is not the owner"); - }); - - it("should revert when the delegator has no active position for the old validator", async function () { - const { vestManager, vestManagerOwner, rewardPool } = await loadFixture(this.fixtures.vestManagerFixture); - - await expect( - vestManager - .connect(vestManagerOwner) - .swapVestedValidator(this.signers.validators[0].address, this.signers.validators[1].address) - ) - .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") - .withArgs("vesting", "OLD_POSITION_INACTIVE"); - }); - - it("should revert that the position is inactive", async function () { - const { systemValidatorSet, vestManager, delegatedValidator, rewardPool, vestManagerOwner } = await loadFixture( - this.fixtures.weeklyVestedDelegationFixture - ); - - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize, - // increase time to make the position maturing - WEEK - ); - - // ensure is not active position - expect(await rewardPool.isActiveDelegatePosition(delegatedValidator.address, vestManager.address), "isActive").be - .false; - - await expect( - vestManager - .connect(vestManagerOwner) - .swapVestedValidator(this.signers.validators[0].address, this.signers.validators[1].address) - ) - .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") - .withArgs("vesting", "OLD_POSITION_INACTIVE"); - }); - - it("should revert when we try to swap to validator with maturing position", async function () { - const { systemValidatorSet, vestManager, liquidToken, vestManagerOwner, rewardPool } = await loadFixture( - this.fixtures.vestManagerFixture - ); - - const oldValidator = this.signers.validators[0]; - const newValidator = this.signers.validators[1]; - await liquidToken.connect(vestManagerOwner).approve(vestManager.address, this.minDelegation); - await vestManager - .connect(vestManagerOwner) - .openVestedDelegatePosition(oldValidator.address, 2, { value: this.minDelegation }); - await vestManager - .connect(vestManagerOwner) - .openVestedDelegatePosition(newValidator.address, 1, { value: this.minDelegation }); - - // commit 8 epochs with 1 day increase before each, so, the first position gonna start maturing - await commitEpochs(systemValidatorSet, rewardPool, [oldValidator, newValidator], 8, this.epochSize, DAY); - - const newPosition = await rewardPool.delegationPositions(newValidator.address, vestManager.address); - expect(newPosition.end.add(newPosition.duration).gt(await time.latest()), "Not matured").to.be.true; - expect(newPosition.end.lt(await time.latest()), "isMaturing").to.be.true; - - await expect( - vestManager.connect(vestManagerOwner).swapVestedValidator(oldValidator.address, newValidator.address) - ) - .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") - .withArgs("vesting", "NEW_POSITION_MATURING"); - }); - - it("should revert when we try to swap to active position (balance > 0)", async function () { - const { vestManager, liquidToken, vestManagerOwner, rewardPool } = await loadFixture( - this.fixtures.vestManagerFixture - ); - - const oldValidator = this.signers.validators[0]; - const newValidator = this.signers.validators[1]; - await liquidToken.connect(vestManagerOwner).approve(vestManager.address, this.minDelegation); - await vestManager - .connect(vestManagerOwner) - .openVestedDelegatePosition(oldValidator.address, 1, { value: this.minDelegation }); - await vestManager - .connect(vestManagerOwner) - .openVestedDelegatePosition(newValidator.address, 1, { value: this.minDelegation }); - - await expect( - vestManager.connect(vestManagerOwner).swapVestedValidator(oldValidator.address, newValidator.address) - ) - .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") - .withArgs("vesting", "INVALID_NEW_POSITION"); - }); - - it("should transfer old position parameters to the new one on successful swap", async function () { - const { systemValidatorSet, rewardPool, vestManager, vestManagerOwner, liquidToken } = await loadFixture( - this.fixtures.vestManagerFixture - ); - - const validator = this.signers.validators[0]; - const newValidator = this.signers.validators[1]; - - const vestingDuration = 2; // 2 weeks - await vestManager.connect(vestManagerOwner).openVestedDelegatePosition(validator.address, vestingDuration, { - value: this.minDelegation.mul(2), - }); - - await commitEpoch(systemValidatorSet, rewardPool, [validator, newValidator], this.epochSize); - - const amount = await rewardPool.delegationOf(validator.address, vestManager.address); - - // give allowance & swap - await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); - await vestManager.connect(vestManagerOwner).swapVestedValidator(validator.address, newValidator.address); - - const oldPosition = await rewardPool.delegationPositions(validator.address, vestManager.address); - const newPosition = await rewardPool.delegationPositions(newValidator.address, vestManager.address); - - // expect new position to be like the old position - expect(oldPosition.duration, "oldPosition.duration").to.be.eq(newPosition.duration); - expect(oldPosition.start, "oldPosition.start").to.be.eq(newPosition.start); - expect(oldPosition.end, "oldPosition.end").to.be.eq(newPosition.end); - expect(oldPosition.base, "oldPosition.base").to.be.eq(newPosition.base); - expect(oldPosition.vestBonus, "oldPosition.vestBonus").to.be.eq(newPosition.vestBonus); - expect(oldPosition.rsiBonus, "oldPosition.rsiBonus").to.be.eq(newPosition.rsiBonus); - }); - - it("should start earning rewards on new position after swap", async function () { - const { systemValidatorSet, rewardPool, vestManager, oldValidator, newValidator } = await loadFixture( - this.fixtures.swappedPositionFixture - ); - - await commitEpoch(systemValidatorSet, rewardPool, [oldValidator, newValidator], this.epochSize); - - const rewardsAfterSwap = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); - expect(rewardsAfterSwap, "rewardsAfterSwap").to.be.gt(0); - }); - - it("should stop earning rewards on old position after swap", async function () { - const { systemValidatorSet, rewardPool, vestManager, oldValidator, newValidator, rewardsBeforeSwap } = - await loadFixture(this.fixtures.swappedPositionFixture); - - await commitEpoch(systemValidatorSet, rewardPool, [oldValidator, newValidator], this.epochSize); - - const rewardsAfterSwap = await rewardPool.getRawDelegatorReward(oldValidator.address, vestManager.address); - expect(rewardsAfterSwap, "rewardsAfterSwap").to.be.eq(rewardsBeforeSwap).and.to.be.gt(0); - }); - - it("should revert when pass incorrect swap index", async function () { - const { systemValidatorSet, rewardPool, vestManager, vestManagerOwner, oldValidator, newValidator } = - await loadFixture(this.fixtures.swappedPositionFixture); - - // commit epochs and increase time to make the position matured & commit epochs - await commitEpochs(systemValidatorSet, rewardPool, [oldValidator, newValidator], 4, this.epochSize, WEEK); - - const rewardsBeforeClaim = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); - expect(rewardsBeforeClaim).to.be.gt(0); - - // prepare params for call - const { epochNum, topUpIndex } = await retrieveRPSData( - systemValidatorSet, - rewardPool, - newValidator.address, - vestManager.address - ); - - await expect( - vestManager.connect(vestManagerOwner).claimVestedPositionReward(newValidator.address, epochNum, topUpIndex + 1) - ) - .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") - .withArgs("vesting", "INVALID_TOP_UP_INDEX"); - }); - - it("should claim all rewards from the new position after swap", async function () { - const { systemValidatorSet, rewardPool, vestManager, vestManagerOwner, oldValidator, newValidator } = - await loadFixture(this.fixtures.swappedPositionFixture); - - // commit epochs and increase time to make the position matured & commit epochs - await commitEpochs(systemValidatorSet, rewardPool, [oldValidator, newValidator], 4, this.epochSize, WEEK); - - const rewardsBeforeClaim = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); - expect(rewardsBeforeClaim).to.be.gt(0); - - // prepare params for call - const { epochNum, topUpIndex } = await retrieveRPSData( - systemValidatorSet, - rewardPool, - newValidator.address, - vestManager.address - ); - - await vestManager.connect(vestManagerOwner).claimVestedPositionReward(newValidator.address, epochNum, topUpIndex); - - const rewardsAfterClaim = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); - expect(rewardsAfterClaim).to.be.eq(0); - }); - - it("should claim all rewards from the old position after swap when the reward is matured", async function () { - const { systemValidatorSet, rewardPool, vestManager, vestManagerOwner, oldValidator, newValidator } = - await loadFixture(this.fixtures.swappedPositionFixture); - - // commit epochs and increase time to make the position matured & commit epochs - await commitEpochs(systemValidatorSet, rewardPool, [oldValidator, newValidator], 3, this.epochSize, WEEK); - - const rewardsBeforeClaim = await rewardPool.getRawDelegatorReward(oldValidator.address, vestManager.address); - expect(rewardsBeforeClaim, "rewardsBeforeClaim").to.be.gt(0); - - // prepare params for call - const { epochNum, topUpIndex } = await retrieveRPSData( - systemValidatorSet, - rewardPool, - oldValidator.address, - vestManager.address - ); - - await vestManager.connect(vestManagerOwner).claimVestedPositionReward(oldValidator.address, epochNum, topUpIndex); - - const rewardsAfterClaim = await rewardPool.getRawDelegatorReward(oldValidator.address, vestManager.address); - expect(rewardsAfterClaim, "rewardsAfterClaim").to.be.eq(0); - }); - - it("should revert when try to swap again during the same epoch", async function () { - const { - rewardPool, - vestManager, - vestManagerOwner, - newValidator: oldValidator, - liquidToken, - } = await loadFixture(this.fixtures.swappedPositionFixture); - - const newValidator = this.signers.validators[2]; - const amount = await rewardPool.delegationOf(oldValidator.address, vestManager.address); - - // give allowance - await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); - - // try to swap - await expect( - vestManager.connect(vestManagerOwner).swapVestedValidator(oldValidator.address, newValidator.address) - ) - .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") - .withArgs("_onAccountParamsChange", "BALANCE_CHANGE_ALREADY_MADE"); - }); - - it("should revert when try to swap too many times", async function () { - const { - systemValidatorSet, - rewardPool, - vestManager, - vestManagerOwner, - newValidator: oldValidator, - liquidToken, - } = await loadFixture(this.fixtures.swappedPositionFixture); - - const newValidator = this.signers.validators[2]; - - await commitEpoch(systemValidatorSet, rewardPool, [oldValidator, newValidator], this.epochSize, 60 * 60); - - const amount = await rewardPool.delegationOf(oldValidator.address, vestManager.address); - - const balanceChangesThreshold = (await rewardPool.balanceChangeThreshold()).toNumber(); - for (let i = 0; i < balanceChangesThreshold; i++) { - const _oldValidator = i % 2 === 0 ? oldValidator : newValidator; - const _newValidator = i % 2 === 0 ? newValidator : oldValidator; - - // give allowance - await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); - await vestManager.connect(vestManagerOwner).swapVestedValidator(_oldValidator.address, _newValidator.address); - await commitEpoch(systemValidatorSet, rewardPool, [oldValidator, newValidator], this.epochSize, 60 * 60); - } - - await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); - - // try to swap to exceed the number of the allowed balance changes - await expect( - vestManager.connect(vestManagerOwner).swapVestedValidator(oldValidator.address, newValidator.address) - ) - .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") - .withArgs("_onAccountParamsChange", "BALANCE_CHANGES_EXCEEDED"); - }); - }); -} diff --git a/test/ValidatorSet/Delegation.test.ts b/test/ValidatorSet/Delegation.test.ts index f103fcbe..fabc09f4 100644 --- a/test/ValidatorSet/Delegation.test.ts +++ b/test/ValidatorSet/Delegation.test.ts @@ -12,8 +12,8 @@ import { RunVestedDelegateClaimTests, RunVestedDelegationRewardsTests, RunDelegateFunctionsByValidatorSet, - RunVestedDelegationSwapTests, } from "../RewardPool/RewardPool.test"; +import { RunSwapVestedPositionValidatorTests } from "./SwapVestedPositionValidator.test"; export function RunDelegationTests(): void { describe("Change minDelegate", function () { @@ -864,7 +864,7 @@ export function RunDelegationTests(): void { }); describe("Reward Pool - Vested delegate swap", async function () { - RunVestedDelegationSwapTests(); + RunSwapVestedPositionValidatorTests(); }); describe("Reward Pool - ValidatorSet protected delegate functions", function () { diff --git a/test/ValidatorSet/SwapVestedPositionValidator.test.ts b/test/ValidatorSet/SwapVestedPositionValidator.test.ts new file mode 100644 index 00000000..fa428ae2 --- /dev/null +++ b/test/ValidatorSet/SwapVestedPositionValidator.test.ts @@ -0,0 +1,287 @@ +/* eslint-disable node/no-extraneous-import */ +import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; + +import { DAY, WEEK } from "../constants"; +import { commitEpoch, commitEpochs, retrieveRPSData } from "../helper"; + +export function RunSwapVestedPositionValidatorTests(): void { + describe("Delegate position rewards", async function () { + it("should revert when not the vest manager owner", async function () { + const { vestManager, delegatedValidator } = await loadFixture(this.fixtures.weeklyVestedDelegationFixture); + + await expect( + vestManager + .connect(this.signers.accounts[10]) + .swapVestedPositionValidator(delegatedValidator.address, delegatedValidator.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("should revert that the position is inactive", async function () { + const { systemValidatorSet, vestManager, delegatedValidator, rewardPool, vestManagerOwner } = await loadFixture( + this.fixtures.weeklyVestedDelegationFixture + ); + + await commitEpoch( + systemValidatorSet, + rewardPool, + [this.signers.validators[0], this.signers.validators[1], delegatedValidator], + this.epochSize, + // increase time to make the position maturing + WEEK + ); + + // ensure is not active position + expect(await rewardPool.isActiveDelegatePosition(delegatedValidator.address, vestManager.address), "isActive").be + .false; + + await expect( + vestManager + .connect(vestManagerOwner) + .swapVestedPositionValidator(this.signers.validators[0].address, this.signers.validators[1].address) + ) + .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") + .withArgs("vesting", "OLD_POSITION_INACTIVE"); + }); + + it("should revert when we try to swap to validator with maturing position", async function () { + const { systemValidatorSet, vestManager, liquidToken, vestManagerOwner, rewardPool } = await loadFixture( + this.fixtures.vestManagerFixture + ); + + const oldValidator = this.signers.validators[0]; + const newValidator = this.signers.validators[1]; + await liquidToken.connect(vestManagerOwner).approve(vestManager.address, this.minDelegation); + await vestManager + .connect(vestManagerOwner) + .openVestedDelegatePosition(oldValidator.address, 2, { value: this.minDelegation }); + await vestManager + .connect(vestManagerOwner) + .openVestedDelegatePosition(newValidator.address, 1, { value: this.minDelegation }); + + // commit 8 epochs with 1 day increase before each, so, the first position gonna start maturing + await commitEpochs(systemValidatorSet, rewardPool, [oldValidator, newValidator], 8, this.epochSize, DAY); + + const newPosition = await rewardPool.delegationPositions(newValidator.address, vestManager.address); + expect(newPosition.end.add(newPosition.duration).gt(await time.latest()), "Not matured").to.be.true; + expect(newPosition.end.lt(await time.latest()), "isMaturing").to.be.true; + + await expect( + vestManager.connect(vestManagerOwner).swapVestedPositionValidator(oldValidator.address, newValidator.address) + ) + .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") + .withArgs("vesting", "NEW_POSITION_MATURING"); + }); + + it("should revert when we try to swap to active position (balance > 0)", async function () { + const { vestManager, liquidToken, vestManagerOwner, rewardPool } = await loadFixture( + this.fixtures.vestManagerFixture + ); + + const oldValidator = this.signers.validators[0]; + const newValidator = this.signers.validators[1]; + await liquidToken.connect(vestManagerOwner).approve(vestManager.address, this.minDelegation); + await vestManager + .connect(vestManagerOwner) + .openVestedDelegatePosition(oldValidator.address, 1, { value: this.minDelegation }); + await vestManager + .connect(vestManagerOwner) + .openVestedDelegatePosition(newValidator.address, 1, { value: this.minDelegation }); + + await expect( + vestManager.connect(vestManagerOwner).swapVestedPositionValidator(oldValidator.address, newValidator.address) + ) + .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") + .withArgs("vesting", "INVALID_NEW_POSITION"); + }); + + it("should transfer old position parameters to the new one on successful swap", async function () { + const { systemValidatorSet, rewardPool, vestManager, vestManagerOwner, liquidToken } = await loadFixture( + this.fixtures.vestManagerFixture + ); + + const validator = this.signers.validators[0]; + const newValidator = this.signers.validators[1]; + + const vestingDuration = 2; // 2 weeks + await vestManager.connect(vestManagerOwner).openVestedDelegatePosition(validator.address, vestingDuration, { + value: this.minDelegation.mul(2), + }); + + await commitEpoch(systemValidatorSet, rewardPool, [validator, newValidator], this.epochSize); + + const amount = await rewardPool.delegationOf(validator.address, vestManager.address); + + // give allowance & swap + await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); + await vestManager.connect(vestManagerOwner).swapVestedPositionValidator(validator.address, newValidator.address); + + const oldPosition = await rewardPool.delegationPositions(validator.address, vestManager.address); + const newPosition = await rewardPool.delegationPositions(newValidator.address, vestManager.address); + + // expect new position to be like the old position + expect(oldPosition.duration, "oldPosition.duration").to.be.eq(newPosition.duration); + expect(oldPosition.start, "oldPosition.start").to.be.eq(newPosition.start); + expect(oldPosition.end, "oldPosition.end").to.be.eq(newPosition.end); + expect(oldPosition.base, "oldPosition.base").to.be.eq(newPosition.base); + expect(oldPosition.vestBonus, "oldPosition.vestBonus").to.be.eq(newPosition.vestBonus); + expect(oldPosition.rsiBonus, "oldPosition.rsiBonus").to.be.eq(newPosition.rsiBonus); + }); + + it("should start earning rewards on new position after swap", async function () { + const { systemValidatorSet, rewardPool, vestManager, oldValidator, newValidator } = await loadFixture( + this.fixtures.swappedPositionFixture + ); + + await commitEpoch(systemValidatorSet, rewardPool, [oldValidator, newValidator], this.epochSize); + + const rewardsAfterSwap = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); + expect(rewardsAfterSwap, "rewardsAfterSwap").to.be.gt(0); + }); + + it("should stop earning rewards on old position after swap", async function () { + const { systemValidatorSet, rewardPool, vestManager, oldValidator, newValidator, rewardsBeforeSwap } = + await loadFixture(this.fixtures.swappedPositionFixture); + + await commitEpoch(systemValidatorSet, rewardPool, [oldValidator, newValidator], this.epochSize); + + const rewardsAfterSwap = await rewardPool.getRawDelegatorReward(oldValidator.address, vestManager.address); + expect(rewardsAfterSwap, "rewardsAfterSwap").to.be.eq(rewardsBeforeSwap).and.to.be.gt(0); + }); + + it("should revert when pass incorrect swap index", async function () { + const { systemValidatorSet, rewardPool, vestManager, vestManagerOwner, oldValidator, newValidator } = + await loadFixture(this.fixtures.swappedPositionFixture); + + // commit epochs and increase time to make the position matured & commit epochs + await commitEpochs(systemValidatorSet, rewardPool, [oldValidator, newValidator], 4, this.epochSize, WEEK); + + const rewardsBeforeClaim = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); + expect(rewardsBeforeClaim).to.be.gt(0); + + // prepare params for call + const { epochNum, topUpIndex } = await retrieveRPSData( + systemValidatorSet, + rewardPool, + newValidator.address, + vestManager.address + ); + + await expect( + vestManager.connect(vestManagerOwner).claimVestedPositionReward(newValidator.address, epochNum, topUpIndex + 1) + ) + .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") + .withArgs("vesting", "INVALID_TOP_UP_INDEX"); + }); + + it("should claim all rewards from the new position after swap", async function () { + const { systemValidatorSet, rewardPool, vestManager, vestManagerOwner, oldValidator, newValidator } = + await loadFixture(this.fixtures.swappedPositionFixture); + + // commit epochs and increase time to make the position matured & commit epochs + await commitEpochs(systemValidatorSet, rewardPool, [oldValidator, newValidator], 4, this.epochSize, WEEK); + + const rewardsBeforeClaim = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); + expect(rewardsBeforeClaim).to.be.gt(0); + + // prepare params for call + const { epochNum, topUpIndex } = await retrieveRPSData( + systemValidatorSet, + rewardPool, + newValidator.address, + vestManager.address + ); + + await vestManager.connect(vestManagerOwner).claimVestedPositionReward(newValidator.address, epochNum, topUpIndex); + + const rewardsAfterClaim = await rewardPool.getRawDelegatorReward(newValidator.address, vestManager.address); + expect(rewardsAfterClaim).to.be.eq(0); + }); + + it("should claim all rewards from the old position after swap when the reward is matured", async function () { + const { systemValidatorSet, rewardPool, vestManager, vestManagerOwner, oldValidator, newValidator } = + await loadFixture(this.fixtures.swappedPositionFixture); + + // commit epochs and increase time to make the position matured & commit epochs + await commitEpochs(systemValidatorSet, rewardPool, [oldValidator, newValidator], 3, this.epochSize, WEEK); + + const rewardsBeforeClaim = await rewardPool.getRawDelegatorReward(oldValidator.address, vestManager.address); + expect(rewardsBeforeClaim, "rewardsBeforeClaim").to.be.gt(0); + + // prepare params for call + const { epochNum, topUpIndex } = await retrieveRPSData( + systemValidatorSet, + rewardPool, + oldValidator.address, + vestManager.address + ); + + await vestManager.connect(vestManagerOwner).claimVestedPositionReward(oldValidator.address, epochNum, topUpIndex); + + const rewardsAfterClaim = await rewardPool.getRawDelegatorReward(oldValidator.address, vestManager.address); + expect(rewardsAfterClaim, "rewardsAfterClaim").to.be.eq(0); + }); + + it("should revert when try to swap again during the same epoch", async function () { + const { + rewardPool, + vestManager, + vestManagerOwner, + newValidator: oldValidator, + liquidToken, + } = await loadFixture(this.fixtures.swappedPositionFixture); + + const newValidator = this.signers.validators[2]; + const amount = await rewardPool.delegationOf(oldValidator.address, vestManager.address); + + // give allowance + await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); + + // try to swap + await expect( + vestManager.connect(vestManagerOwner).swapVestedPositionValidator(oldValidator.address, newValidator.address) + ) + .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") + .withArgs("_saveAccountParamsChange", "BALANCE_CHANGE_ALREADY_MADE"); + }); + + it("should revert when try to swap too many times", async function () { + const { + systemValidatorSet, + rewardPool, + vestManager, + vestManagerOwner, + newValidator: oldValidator, + liquidToken, + } = await loadFixture(this.fixtures.swappedPositionFixture); + + const newValidator = this.signers.validators[2]; + + await commitEpoch(systemValidatorSet, rewardPool, [oldValidator, newValidator], this.epochSize, 60 * 60); + + const amount = await rewardPool.delegationOf(oldValidator.address, vestManager.address); + + const balanceChangesThreshold = (await rewardPool.balanceChangeThreshold()).toNumber(); + for (let i = 0; i < balanceChangesThreshold; i++) { + const _oldValidator = i % 2 === 0 ? oldValidator : newValidator; + const _newValidator = i % 2 === 0 ? newValidator : oldValidator; + + // give allowance + await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); + await vestManager + .connect(vestManagerOwner) + .swapVestedPositionValidator(_oldValidator.address, _newValidator.address); + await commitEpoch(systemValidatorSet, rewardPool, [oldValidator, newValidator], this.epochSize, 60 * 60); + } + + await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); + + // try to swap to exceed the number of the allowed balance changes + await expect( + vestManager.connect(vestManagerOwner).swapVestedPositionValidator(oldValidator.address, newValidator.address) + ) + .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") + .withArgs("_saveAccountParamsChange", "BALANCE_CHANGES_EXCEEDED"); + }); + }); +} diff --git a/test/fixtures.ts b/test/fixtures.ts index a9482c5f..8f847c16 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -482,7 +482,7 @@ async function swappedPositionFixtureFunction(this: Mocha.Context) { // give allowance & swap await liquidToken.connect(vestManagerOwner).approve(vestManager.address, amount); - await vestManager.connect(vestManagerOwner).swapVestedValidator(validator.address, newValidator.address); + await vestManager.connect(vestManagerOwner).swapVestedPositionValidator(validator.address, newValidator.address); return { validatorSet, From aa9b2ddcb213a876ca0dc0a2fe042ace85375acc Mon Sep 17 00:00:00 2001 From: Vitomir Pavlov Date: Thu, 6 Jun 2024 15:34:42 +0300 Subject: [PATCH 6/7] swap defence optimization add more checks in the swap function to avoid some attack vectors; create a separate function to check if the new position is available and revert with general error; delete the top-up tests; cover the new checks in the tests; --- .../RewardPool/modules/DelegationRewards.sol | 38 +- docs/RewardPool/RewardPool.md | 23 + docs/RewardPool/modules/DelegationRewards.md | 23 + .../modules/PowerExponent/PowerExponent.md | 467 ++++++++++++++++++ test/RewardPool/RewardPool.test.ts | 456 +---------------- test/ValidatorSet/Delegation.test.ts | 176 +------ .../SwapVestedPositionValidator.test.ts | 101 +++- test/constants.ts | 3 + 8 files changed, 637 insertions(+), 650 deletions(-) diff --git a/contracts/RewardPool/modules/DelegationRewards.sol b/contracts/RewardPool/modules/DelegationRewards.sol index 45678b13..f8cd5ac5 100644 --- a/contracts/RewardPool/modules/DelegationRewards.sol +++ b/contracts/RewardPool/modules/DelegationRewards.sol @@ -323,15 +323,9 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa revert DelegateRequirement({src: "vesting", msg: "OLD_POSITION_INACTIVE"}); } - VestingPosition memory newPosition = delegationPositions[newValidator][delegator]; - if (newPosition.isMaturing()) { - revert DelegateRequirement({src: "vesting", msg: "NEW_POSITION_MATURING"}); - } - - DelegationPool storage newDelegation = delegationPools[newValidator]; - uint256 balance = newDelegation.balanceOf(delegator); - if (balance != 0) { - revert DelegateRequirement({src: "vesting", msg: "INVALID_NEW_POSITION"}); + // ensure that the new position is available + if (!isPositionAvailable(newValidator, delegator)) { + revert DelegateRequirement({src: "vesting", msg: "NEW_POSITION_UNAVAILABLE"}); } // update the old delegation position @@ -346,9 +340,11 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa DelegationPoolParams({balance: 0, correction: correction, epochNum: currentEpochId}) ); - // set the new delegation position using the old position parameters + DelegationPool storage newDelegation = delegationPools[newValidator]; + // deposit the old amount to the new position newDelegation.deposit(delegator, amount); + // transfer the old position parameters to the new one delegationPositions[newValidator][delegator] = VestingPosition({ duration: oldPosition.duration, start: oldPosition.start, @@ -484,6 +480,7 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa return false; } + // TODO: Consider deleting it as we shouldn't be getting into that case /** * @notice Checks if the balance changes exceeds the threshold * @param validator Validator to delegate to @@ -493,6 +490,27 @@ abstract contract DelegationRewards is RewardPoolBase, Vesting, RewardsWithdrawa return delegationPoolParamsHistory[validator][delegator].length > balanceChangeThreshold; } + /** + * @notice Check if the new position that the user wants to swap to is available for the swap + * @dev Available positions one that is not active, not maturing and doesn't have any left balance or rewards + * @param newValidator The address of the new validator + * @param delegator The address of the delegator + */ + function isPositionAvailable(address newValidator, address delegator) public view returns (bool) { + VestingPosition memory newPosition = delegationPositions[newValidator][delegator]; + if (newPosition.isActive() || newPosition.isMaturing()) { + return false; + } + + DelegationPool storage newDelegation = delegationPools[newValidator]; + uint256 balance = newDelegation.balanceOf(delegator); + if (balance != 0 || getRawDelegatorReward(newValidator, delegator) > 0) { + return false; + } + + return true; + } + // _______________ Private functions _______________ function _changeMinDelegation(uint256 newMinDelegation) private { diff --git a/docs/RewardPool/RewardPool.md b/docs/RewardPool/RewardPool.md index 39ce12f8..f4a1fee9 100644 --- a/docs/RewardPool/RewardPool.md +++ b/docs/RewardPool/RewardPool.md @@ -1097,6 +1097,29 @@ function isMaturingPosition(address staker) external view returns (bool) |---|---|---| | _0 | bool | undefined | +### isPositionAvailable + +```solidity +function isPositionAvailable(address newValidator, address delegator) external view returns (bool) +``` + +Check if the new position that the user wants to swap to is available for the swap + +*Available positions one that is not active, not maturing and doesn't have any left balance or rewards* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| newValidator | address | The address of the new validator | +| delegator | address | The address of the delegator | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined | + ### isStakerInVestingCycle ```solidity diff --git a/docs/RewardPool/modules/DelegationRewards.md b/docs/RewardPool/modules/DelegationRewards.md index 0a2d6045..55aa33a4 100644 --- a/docs/RewardPool/modules/DelegationRewards.md +++ b/docs/RewardPool/modules/DelegationRewards.md @@ -949,6 +949,29 @@ function isMaturingPosition(address staker) external view returns (bool) |---|---|---| | _0 | bool | undefined | +### isPositionAvailable + +```solidity +function isPositionAvailable(address newValidator, address delegator) external view returns (bool) +``` + +Check if the new position that the user wants to swap to is available for the swap + +*Available positions one that is not active, not maturing and doesn't have any left balance or rewards* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| newValidator | address | The address of the new validator | +| delegator | address | The address of the delegator | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined | + ### isStakerInVestingCycle ```solidity diff --git a/docs/ValidatorSet/modules/PowerExponent/PowerExponent.md b/docs/ValidatorSet/modules/PowerExponent/PowerExponent.md index a05a85de..e37ccb24 100644 --- a/docs/ValidatorSet/modules/PowerExponent/PowerExponent.md +++ b/docs/ValidatorSet/modules/PowerExponent/PowerExponent.md @@ -10,6 +10,23 @@ ## Methods +### DOMAIN + +```solidity +function DOMAIN() external view returns (bytes32) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bytes32 | undefined | + ### acceptOwnership ```solidity @@ -21,6 +38,180 @@ function acceptOwnership() external nonpayable *The new owner accepts the ownership transfer.* +### activeValidatorsCount + +```solidity +function activeValidatorsCount() external view returns (uint256) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +### addToWhitelist + +```solidity +function addToWhitelist(address[] whitelistAddreses) external nonpayable +``` + +Adds addresses that are allowed to register as validators. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| whitelistAddreses | address[] | Array of address to whitelist | + +### balanceOf + +```solidity +function balanceOf(address account) external view returns (uint256) +``` + +Returns the total balance of a given validator + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| account | address | The address of the validator | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | Validator's balance | + +### bls + +```solidity +function bls() external view returns (contract IBLS) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | contract IBLS | undefined | + +### currentEpochId + +```solidity +function currentEpochId() external view returns (uint256) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +### epochEndBlocks + +```solidity +function epochEndBlocks(uint256) external view returns (uint256) +``` + +Array with epoch ending blocks + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +### epochs + +```solidity +function epochs(uint256) external view returns (uint256 startBlock, uint256 endBlock, bytes32 epochRoot) +``` + +Epoch data linked with the epoch id + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| startBlock | uint256 | undefined | +| endBlock | uint256 | undefined | +| epochRoot | bytes32 | undefined | + +### getActiveValidatorsCount + +```solidity +function getActiveValidatorsCount() external view returns (uint256) +``` + +Gets the number of current validators + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | Returns the count as uint256 | + +### getEpochByBlock + +```solidity +function getEpochByBlock(uint256 blockNumber) external view returns (struct Epoch) +``` + +Look up an epoch by block number. Searches in O(log n) time. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| blockNumber | uint256 | ID of epoch to be committed | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | Epoch | Epoch Returns epoch if found, or else, the last epoch | + ### getExponent ```solidity @@ -39,6 +230,50 @@ Return the Voting Power Exponent Numerator and Denominator | numerator | uint256 | undefined | | denominator | uint256 | undefined | +### getValidator + +```solidity +function getValidator(address validator) external view returns (uint256[4] blsKey, uint256 stake, uint256 totalStake, uint256 commission, uint256 withdrawableRewards, bool active) +``` + +Gets validator by address. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| validator | address | Address of the validator | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| blsKey | uint256[4] | BLS public key | +| stake | uint256 | self-stake | +| totalStake | uint256 | self-stake + delegation | +| commission | uint256 | commission | +| withdrawableRewards | uint256 | withdrawable rewards | +| active | bool | activity status | + +### getValidators + +```solidity +function getValidators() external view returns (address[]) +``` + +Gets all validators. Returns already unactive validators as well. + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address[] | Returns array of addresses | + ### owner ```solidity @@ -91,6 +326,22 @@ function powerExponent() external view returns (uint128 value, uint128 pendingVa | value | uint128 | undefined | | pendingValue | uint128 | undefined | +### removeFromWhitelist + +```solidity +function removeFromWhitelist(address[] whitelistAddreses) external nonpayable +``` + +Deletes addresses that are allowed to register as validators. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| whitelistAddreses | address[] | Array of address to remove from whitelist | + ### renounceOwnership ```solidity @@ -102,6 +353,62 @@ function renounceOwnership() external nonpayable *Leaves the contract without owner. It will not be possible to call `onlyOwner` functions. Can only be called by the current owner. NOTE: Renouncing ownership will leave the contract without an owner, thereby disabling any functionality that is only available to the owner.* +### rewardPool + +```solidity +function rewardPool() external view returns (contract IRewardPool) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | contract IRewardPool | undefined | + +### totalBlocks + +```solidity +function totalBlocks(uint256 epochId) external view returns (uint256 length) +``` + +Total amount of blocks in a given epoch + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| epochId | uint256 | The number of the epoch | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| length | uint256 | Total blocks for an epoch | + +### totalSupply + +```solidity +function totalSupply() external view returns (uint256) +``` + +Returns the total supply + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | Total supply | + ### transferOwnership ```solidity @@ -134,10 +441,110 @@ Set new pending exponent, to be activated in the next commit epoch |---|---|---| | newValue | uint256 | New Voting Power Exponent Numerator | +### updateValidatorParticipation + +```solidity +function updateValidatorParticipation(address validator) external nonpayable +``` + +Method to update when the validator was lastly active which can be executed only by the RewardPool + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| validator | address | The validator to set the last participation for | + +### validatorParticipation + +```solidity +function validatorParticipation(address) external view returns (uint256) +``` + +Mapping that keeps the last time when a validator has participated in the consensus + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +### validators + +```solidity +function validators(address) external view returns (uint256 liquidDebt, uint256 commission, enum ValidatorStatus status) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| liquidDebt | uint256 | undefined | +| commission | uint256 | undefined | +| status | enum ValidatorStatus | undefined | + +### validatorsAddresses + +```solidity +function validatorsAddresses(uint256) external view returns (address) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined | + ## Events +### AddedToWhitelist + +```solidity +event AddedToWhitelist(address indexed validator) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| validator `indexed` | address | undefined | + ### Initialized ```solidity @@ -154,6 +561,25 @@ event Initialized(uint8 version) |---|---|---| | version | uint8 | undefined | +### NewEpoch + +```solidity +event NewEpoch(uint256 indexed id, uint256 indexed startBlock, uint256 indexed endBlock, bytes32 epochRoot) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| id `indexed` | uint256 | undefined | +| startBlock `indexed` | uint256 | undefined | +| endBlock `indexed` | uint256 | undefined | +| epochRoot | bytes32 | undefined | + ### OwnershipTransferStarted ```solidity @@ -188,5 +614,46 @@ event OwnershipTransferred(address indexed previousOwner, address indexed newOwn | previousOwner `indexed` | address | undefined | | newOwner `indexed` | address | undefined | +### RemovedFromWhitelist + +```solidity +event RemovedFromWhitelist(address indexed validator) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| validator `indexed` | address | undefined | + + + +## Errors + +### MustBeWhitelisted + +```solidity +error MustBeWhitelisted() +``` + + + + + + +### PreviouslyWhitelisted + +```solidity +error PreviouslyWhitelisted() +``` + + + + + diff --git a/test/RewardPool/RewardPool.test.ts b/test/RewardPool/RewardPool.test.ts index e51b0a9b..6ffc41d5 100644 --- a/test/RewardPool/RewardPool.test.ts +++ b/test/RewardPool/RewardPool.test.ts @@ -3,7 +3,7 @@ import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers"; import * as hre from "hardhat"; import { expect } from "chai"; -import { DAY, EPOCHS_YEAR, ERRORS, MIN_RSI_BONUS, VESTING_DURATION_WEEKS, WEEK } from "../constants"; +import { DAY, EPOCHS_YEAR, ERRORS, VESTING_DURATION_WEEKS, WEEK } from "../constants"; import { calculateExpectedReward, commitEpoch, @@ -638,459 +638,5 @@ export function RunVestedDelegateClaimTests(): void { [maxFinalReward.sub(expectedFinalReward), expectedFinalReward, maxFinalReward.mul(-1)] ); }); - - // TODO: Delete when remove the top-up functionality - it.skip("should properly claim reward when top-ups and not full reward matured", async function () { - const { systemValidatorSet, validatorSet, rewardPool, vestManager, vestManagerOwner, delegatedValidator } = - await loadFixture(this.fixtures.weeklyVestedDelegationFixture); - - // calculate base rewards - const baseReward = await rewardPool.getRawDelegatorReward(delegatedValidator.address, vestManager.address); - const base = await rewardPool.base(); - const vestBonus = await rewardPool.getVestingBonus(1); - const rsi = await rewardPool.rsi(); - const expectedBaseReward = await calculateExpectedReward(base, vestBonus, rsi, baseReward); - - // top-up - await vestManager.topUpVestedDelegatePosition(delegatedValidator.address, { - value: this.minDelegation, - }); - - const topUpRewardsTimestamp = await time.latest(); - const position = await rewardPool.delegationPositions(delegatedValidator.address, vestManager.address); - const toBeMatured = hre.ethers.BigNumber.from(topUpRewardsTimestamp).sub(position.start); - // more rewards to be distributed but with the top-up data - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize - ); - - // calculate top-up reward - const topUpReward = (await rewardPool.getRawDelegatorReward(delegatedValidator.address, vestManager.address)).sub( - baseReward - ); - - // no rsi because top-up is used - const expectedTopUpReward = await calculateExpectedReward(base, vestBonus, MIN_RSI_BONUS, topUpReward); - const expectedReward = expectedBaseReward.add(expectedTopUpReward); - - // calculate max reward - const maxVestBonus = await rewardPool.getVestingBonus(52); - const maxRSI = await rewardPool.MAX_RSI_BONUS(); - const maxBaseReward = await calculateExpectedReward(base, maxVestBonus, maxRSI, baseReward); - const maxTopUpReward = await calculateExpectedReward(base, maxVestBonus, maxRSI, topUpReward); - const maxReward = maxBaseReward.add(maxTopUpReward); - - // commit epoch, so more reward is added that must not be claimed now - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize, - // enter the maturing state - // two week is the duration + the needed time for the top-up to be matured - WEEK * 2 + toBeMatured.toNumber() + 1 - ); - - // prepare params for call - const { epochNum, topUpIndex } = await retrieveRPSData( - validatorSet, - rewardPool, - delegatedValidator.address, - vestManager.address - ); - - const areRewardsMatured = position.end.add(toBeMatured).lt(await time.latest()); - expect(areRewardsMatured, "areRewardsMatured").to.be.true; - - await expect( - await vestManager.claimVestedPositionReward(delegatedValidator.address, epochNum, topUpIndex), - "claimVestedPositionReward" - ).to.changeEtherBalances( - [hre.ethers.constants.AddressZero, vestManagerOwner.address, rewardPool.address], - [maxReward.sub(expectedReward), expectedReward, maxReward.mul(-1)] - ); - }); - - // TODO: Delete when remove the top-up functionality - it.skip("should properly claim reward when top-ups and full reward matured", async function () { - const { systemValidatorSet, validatorSet, rewardPool, vestManager, vestManagerOwner, delegatedValidator } = - await loadFixture(this.fixtures.weeklyVestedDelegationFixture); - - // calculate base rewards - const baseReward = await rewardPool.getRawDelegatorReward(delegatedValidator.address, vestManager.address); - const base = await rewardPool.base(); - const vestBonus = await rewardPool.getVestingBonus(1); - const rsi = await rewardPool.rsi(); - const expectedBaseReward = await calculateExpectedReward(base, vestBonus, rsi, baseReward); - - // top-up - await vestManager.topUpVestedDelegatePosition(delegatedValidator.address, { value: this.minDelegation }); - - // more rewards to be distributed but with the top-up data - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize - ); - - // calculate top-up reward - const topUpReward = (await rewardPool.getRawDelegatorReward(delegatedValidator.address, vestManager.address)).sub( - baseReward - ); - const expectedTopUpReward = await calculateExpectedReward(base, vestBonus, MIN_RSI_BONUS, topUpReward); - - const expectedReward = expectedBaseReward.add(expectedTopUpReward); - - // calculate max reward - const maxRSI = await rewardPool.MAX_RSI_BONUS(); - const maxVestBonus = await rewardPool.getVestingBonus(52); - const maxBaseReward = await calculateExpectedReward(base, maxVestBonus, maxRSI, baseReward); - const maxTopUpReward = await calculateExpectedReward(base, maxVestBonus, maxRSI, topUpReward); - - const maxReward = maxBaseReward.add(maxTopUpReward); - - // commit epoch, so more reward is added that must be without bonus - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize, - // enter the maturing state - // 52 weeks is the duration + the needed time for the top-up to be matured - WEEK * 104 * 4 + 1 - ); - - const additionalReward = ( - await rewardPool.getRawDelegatorReward(delegatedValidator.address, vestManager.address) - ).sub(baseReward.add(topUpReward)); - - const expectedAdditionalReward = base.mul(additionalReward).div(10000).div(EPOCHS_YEAR); - const maxAdditionalReward = await calculateExpectedReward(base, maxVestBonus, maxRSI, additionalReward); - - // prepare params for call - let { position, epochNum, topUpIndex } = await retrieveRPSData( - validatorSet, - rewardPool, - delegatedValidator.address, - vestManager.address - ); - - // 1 because we have only one top-up, but the first is for the openDelegatorPosition - topUpIndex = 1; - - // ensure rewards are matured - const areRewardsMatured = position.end.add(position.duration).lt(await time.latest()); - expect(areRewardsMatured, "areRewardsMatured").to.be.true; - - const expectedFinalReward = expectedReward.add(expectedAdditionalReward); - const maxFinalReward = maxReward.add(maxAdditionalReward); - - await expect( - await vestManager.claimVestedPositionReward(delegatedValidator.address, epochNum, topUpIndex), - "claimVestedPositionReward" - ).to.changeEtherBalances( - [hre.ethers.constants.AddressZero, vestManagerOwner.address, rewardPool.address], - [maxFinalReward.sub(expectedFinalReward), expectedFinalReward, maxFinalReward.mul(-1)] - ); - }); - - // TODO: Delete when remove the top-up functionality - it.skip("should revert when invalid top-up index", async function () { - const { systemValidatorSet, validatorSet, rewardPool, vestManager, delegatedValidator } = await loadFixture( - this.fixtures.weeklyVestedDelegationFixture - ); - - // top-up - await vestManager.topUpVestedDelegatePosition(delegatedValidator.address, { value: this.minDelegation }); - - // more rewards to be distributed but with the top-up data - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize - ); - - const topUpRewardsTimestamp = await time.latest(); - const position = await rewardPool.delegationPositions(delegatedValidator.address, vestManager.address); - const toBeMatured = hre.ethers.BigNumber.from(topUpRewardsTimestamp).sub(position.start); - - // comit epoch, so more reward is added that must not be claimed now - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize, - // enter the maturing state - // two week is the duration + the needed time for the top-up to be matured - WEEK * 104 + toBeMatured.toNumber() + 1 - ); - - // prepare params for call - let { epochNum, topUpIndex } = await retrieveRPSData( - validatorSet, - rewardPool, - delegatedValidator.address, - vestManager.address - ); - - // set invalid index - topUpIndex = 2; - - // ensure rewards are maturing - const areRewardsMatured = position.end.add(toBeMatured).lt(await time.latest()); - expect(areRewardsMatured).to.be.true; - - await expect(vestManager.claimVestedPositionReward(delegatedValidator.address, epochNum, topUpIndex)) - .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") - .withArgs("vesting", "INVALID_TOP_UP_INDEX"); - }); - - // TODO: Delete when remove the top-up functionality - it.skip("should revert when later top-up index", async function () { - const { systemValidatorSet, validatorSet, rewardPool, vestManager, delegatedValidator } = await loadFixture( - this.fixtures.weeklyVestedDelegationFixture - ); - - // top-up - await vestManager.topUpVestedDelegatePosition(delegatedValidator.address, { value: this.minDelegation }); - - // more rewards to be distributed but with the top-up data - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize - ); - - // add another top-up - await vestManager.topUpVestedDelegatePosition(delegatedValidator.address, { value: this.minDelegation }); - - // more rewards to be distributed but with the top-up data - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize - ); - - // commit epoch, so more reward is added that must not be claimed now - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize, - // enter the maturing state - // 52 weeks is the duration + the needed time for the top-up to be matured - WEEK * 104 + 1 - ); - - // prepare params for call - let { epochNum, topUpIndex } = await retrieveRPSData( - validatorSet, - rewardPool, - delegatedValidator.address, - vestManager.address - ); - - // set later index - topUpIndex = 2; - - await expect(vestManager.claimVestedPositionReward(delegatedValidator.address, epochNum - 1, topUpIndex)) - .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") - .withArgs("vesting", "LATER_TOP_UP"); - }); - - // TODO: Delete when remove the top-up functionality - it.skip("should revert when earlier top-up index", async function () { - const { systemValidatorSet, validatorSet, rewardPool, vestManager, delegatedValidator } = await loadFixture( - this.fixtures.weeklyVestedDelegationFixture - ); - - // top-up - await vestManager.topUpVestedDelegatePosition(delegatedValidator.address, { value: this.minDelegation }); - - // more rewards to be distributed but with the top-up data - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize - ); - - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize - ); - - await vestManager.topUpVestedDelegatePosition(delegatedValidator.address, { value: this.minDelegation }); - - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize - ); - - // enter the maturing state - // reward to be matured - await time.increase(WEEK * 104); - - // prepare params for call - const { epochNum, topUpIndex } = await retrieveRPSData( - validatorSet, - rewardPool, - delegatedValidator.address, - vestManager.address - ); - - await expect(vestManager.claimVestedPositionReward(delegatedValidator.address, epochNum, topUpIndex)) - .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") - .withArgs("vesting", "EARLIER_TOP_UP"); - }); - - // TODO: Delete when remove the top-up functionality - it.skip("should claim only reward made before top-up", async function () { - const { systemValidatorSet, validatorSet, rewardPool, vestManager, vestManagerOwner, delegatedValidator } = - await loadFixture(this.fixtures.weeklyVestedDelegationFixture); - - // calculate base rewards - const baseReward = await rewardPool.getRawDelegatorReward(delegatedValidator.address, vestManager.address); - const base = await rewardPool.base(); - const vestBonus = await rewardPool.getVestingBonus(1); - const rsi = await rewardPool.rsi(); - const expectedBaseReward = await calculateExpectedReward(base, vestBonus, rsi, baseReward); - - const maxRSI = await rewardPool.MAX_RSI_BONUS(); - const maxVestBonus = await rewardPool.getVestingBonus(52); - const maxBaseReward = await calculateExpectedReward(base, maxVestBonus, maxRSI, baseReward); - - const rewardDistributionTime = await time.latest(); - let position = await rewardPool.delegationPositions(delegatedValidator.address, vestManager.address); - const toBeMatured = hre.ethers.BigNumber.from(rewardDistributionTime).sub(position.start); - await time.increase(50); - - // top-up - await vestManager.topUpVestedDelegatePosition(delegatedValidator.address, { value: this.minDelegation }); - - // more rewards to be distributed but with the top-up data - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize - ); - - // commit epoch, so more reward is added that must be without bonus - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize - ); - - position = await rewardPool.delegationPositions(delegatedValidator.address, vestManager.address); - // enter the maturing state - await time.increaseTo(position.end.toNumber() + toBeMatured.toNumber() + 1); - - // prepare params for call - const currentEpochId = await validatorSet.currentEpochId(); - const rpsValues = await rewardPool.getRPSValues(delegatedValidator.address, 0, currentEpochId); - const epochNum = findProperRPSIndex(rpsValues, position.start.add(toBeMatured)); - const topUpIndex = 0; - - // ensure rewards are maturing - const areRewardsMaturing = position.end.add(toBeMatured).lt(await time.latest()); - expect(areRewardsMaturing).to.be.true; - - await expect( - await vestManager.claimVestedPositionReward(delegatedValidator.address, epochNum, topUpIndex), - "claimVestedPositionReward" - ).to.changeEtherBalances( - [hre.ethers.constants.AddressZero, vestManagerOwner.address, rewardPool.address], - [maxBaseReward.sub(expectedBaseReward), expectedBaseReward, maxBaseReward.mul(-1)] - ); - }); - - // TODO: Delete when remove the top-up functionality - it.skip("should claim rewards multiple times", async function () { - const { systemValidatorSet, validatorSet, rewardPool, vestManager, vestManagerOwner, delegatedValidator } = - await loadFixture(this.fixtures.weeklyVestedDelegationFixture); - - // calculate rewards - const baseReward = await rewardPool.getRawDelegatorReward(delegatedValidator.address, vestManager.address); - const base = await rewardPool.base(); - const vestBonus = await rewardPool.getVestingBonus(1); - const rsi = await rewardPool.rsi(); - const reward = await calculateExpectedReward(base, vestBonus, rsi, baseReward); - - const maxRSI = await rewardPool.MAX_RSI_BONUS(); - const maxVestBonus = await rewardPool.getVestingBonus(52); - const maxBaseReward = await calculateExpectedReward(base, maxVestBonus, maxRSI, baseReward); - - const rewardDistributionTime = await time.latest(); - let position = await rewardPool.delegationPositions(delegatedValidator.address, vestManager.address); - const toBeMatured = hre.ethers.BigNumber.from(rewardDistributionTime).sub(position.start); - await time.increase(50); - - // top-up - await vestManager.topUpVestedDelegatePosition(delegatedValidator.address, { value: this.minDelegation }); - - // more rewards to be distributed but with the top-up data - await commitEpochs( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - 2, - this.epochSize - ); - - position = await rewardPool.delegationPositions(delegatedValidator.address, vestManager.address); - - // enter the maturing state - await time.increaseTo(position.end.toNumber() + toBeMatured.toNumber() + 1); - - // prepare params for call - const currentEpochId = await validatorSet.currentEpochId(); - const rpsValues = await rewardPool.getRPSValues(delegatedValidator.address, 0, currentEpochId); - const epochNum = findProperRPSIndex(rpsValues, position.start.add(toBeMatured)); - const topUpIndex = 0; - - // ensure rewards are maturing - const areRewardsMaturing = position.end.add(toBeMatured).lt(await time.latest()); - expect(areRewardsMaturing).to.be.true; - - await expect( - await vestManager.claimVestedPositionReward(delegatedValidator.address, epochNum, topUpIndex), - "claimVestedPositionReward" - ).to.changeEtherBalances([vestManagerOwner.address, rewardPool.address], [reward, maxBaseReward.mul(-1)]); - - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize, - WEEK * 2 - ); - expect(await vestManager.claimVestedPositionReward(delegatedValidator.address, epochNum + 1, topUpIndex + 1)).to - .not.be.reverted; - - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], delegatedValidator], - this.epochSize, - WEEK * 52 - ); - - expect(await vestManager.claimVestedPositionReward(delegatedValidator.address, epochNum + 1, topUpIndex + 1)).to - .not.be.reverted; - }); }); } diff --git a/test/ValidatorSet/Delegation.test.ts b/test/ValidatorSet/Delegation.test.ts index fabc09f4..bd018e81 100644 --- a/test/ValidatorSet/Delegation.test.ts +++ b/test/ValidatorSet/Delegation.test.ts @@ -6,7 +6,7 @@ import * as hre from "hardhat"; // eslint-disable-next-line camelcase import { VestManager__factory } from "../../typechain-types"; import { ERRORS, VESTING_DURATION_WEEKS, WEEK } from "../constants"; -import { calculatePenalty, claimPositionRewards, commitEpoch, commitEpochs, getUserManager } from "../helper"; +import { calculatePenalty, claimPositionRewards, commitEpochs, getUserManager } from "../helper"; import { RunDelegateClaimTests, RunVestedDelegateClaimTests, @@ -681,180 +681,6 @@ export function RunDelegationTests(): void { }); }); - // TODO: Delete when remove the top-up functionality - describe.skip("topUpVestedDelegatePosition()", async function () { - it("should revert when not owner of the vest manager", async function () { - const { vestManager } = await loadFixture(this.fixtures.vestedDelegationFixture); - - await expect( - vestManager - .connect(this.signers.accounts[10]) - .topUpVestedDelegatePosition(this.signers.accounts[10].address, { value: this.minDelegation }) - ).to.be.revertedWith(ERRORS.ownable); - }); - - it("should revert when not manager", async function () { - const { validatorSet } = await loadFixture(this.fixtures.vestedDelegationFixture); - - await expect( - validatorSet - .connect(this.signers.accounts[10]) - .topUpDelegatePosition(this.signers.accounts[10].address, { value: this.minDelegation }) - ).to.be.revertedWithCustomError(validatorSet, "NotVestingManager"); - }); - - it("should revert when delegation too low", async function () { - const { validatorSet, vestManager } = await loadFixture(this.fixtures.vestedDelegationFixture); - - await expect( - vestManager.topUpVestedDelegatePosition(this.signers.validators[0].address, { - value: this.minDelegation.sub(1), - }) - ) - .to.be.revertedWithCustomError(validatorSet, "DelegateRequirement") - .withArgs("vesting", "DELEGATION_TOO_LOW"); - }); - - it("should revert when position is not active", async function () { - const { validatorSet, vestManager, delegatedValidator } = await loadFixture( - this.fixtures.vestedDelegationFixture - ); - - // enter the reward maturity phase in order to make the position inactive - await time.increase(WEEK * 55); - await expect(vestManager.topUpVestedDelegatePosition(delegatedValidator.address, { value: this.minDelegation })) - .to.be.revertedWithCustomError(validatorSet, "DelegateRequirement") - .withArgs("vesting", "POSITION_NOT_ACTIVE"); - }); - - it("should properly top-up position", async function () { - const { systemValidatorSet, validatorSet, rewardPool, vestManager } = await loadFixture( - this.fixtures.vestManagerFixture - ); - - const duration = 1; // 1 week - await vestManager.openVestedDelegatePosition(this.delegatedValidators[0], duration, { - value: this.minDelegation, - }); - const positionEndBefore = ( - await rewardPool.delegationPositions(this.delegatedValidators[0], vestManager.address) - ).end; - - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], this.signers.validators[2]], - this.epochSize, - 1 // increase with 1 second to enter the active state - ); - - // ensure position is active - const isActive = await rewardPool.isActiveDelegatePosition(this.delegatedValidators[0], vestManager.address); - expect(isActive, "isActive").to.be.true; - - const delegatedAmount = await rewardPool.delegationOf(this.delegatedValidators[0], vestManager.address); - const topUpAmount = this.minDelegation.div(2); - const totalAmount = delegatedAmount.add(topUpAmount); - - await vestManager.topUpVestedDelegatePosition(this.delegatedValidators[0], { value: topUpAmount }); - - // delegation is increased - expect( - await rewardPool.delegationOf(this.delegatedValidators[0], vestManager.address), - "delegationOf" - ).to.be.eq(totalAmount); - - // balance change data is added - const balanceChange = await rewardPool.delegationPoolParamsHistory( - this.delegatedValidators[0], - vestManager.address, - 1 - ); - expect(balanceChange.balance, "balanceChange.balance").to.be.eq(totalAmount); - expect(balanceChange.epochNum, "balanceChange.epochNum").to.be.eq(await validatorSet.currentEpochId()); - - // duration increase is proper - const positionEndAfter = ( - await rewardPool.delegationPositions(this.delegatedValidators[0], vestManager.address) - ).end; - expect(positionEndAfter).to.be.eq(positionEndBefore.add((duration * WEEK) / 2)); - }); - - it("should revert when too many top-ups are made", async function () { - const { systemValidatorSet, validatorSet, rewardPool, vestManager } = await loadFixture( - this.fixtures.vestedDelegationFixture - ); - - const maxTopUps = 52; // one cannot top-up more than 52 times - for (let i = 0; i < maxTopUps; i++) { - const delegatingAmount = this.minDelegation.mul(i + 1).div(5); - await vestManager.topUpVestedDelegatePosition(this.delegatedValidators[0], { value: delegatingAmount }); - - // commit epoch cause only 1 top-up can be made per epoch - await commitEpoch( - systemValidatorSet, - rewardPool, - [this.signers.validators[0], this.signers.validators[1], this.signers.validators[2]], - this.epochSize - ); - } - - await expect( - vestManager.topUpVestedDelegatePosition(this.delegatedValidators[0], { value: this.minDelegation }) - ) - .to.be.revertedWithCustomError(validatorSet, "DelegateRequirement") - .withArgs("vesting", "BALANCE_CHANGES_EXCEEDED"); - }); - - it("should revert when top-up already made in the same epoch", async function () { - const { validatorSet, vestManager } = await loadFixture(this.fixtures.vestedDelegationFixture); - - await vestManager.topUpVestedDelegatePosition(this.delegatedValidators[0], { value: this.minDelegation }); - - await expect( - vestManager.topUpVestedDelegatePosition(this.delegatedValidators[0], { value: this.minDelegation }) - ) - .to.be.revertedWithCustomError(validatorSet, "DelegateRequirement") - .withArgs("_onAccountParamsChange", "BALANCE_CHANGE_ALREADY_MADE"); - }); - - it("should increase duration no more than 100%", async function () { - const { rewardPool, vestManager } = await loadFixture(this.fixtures.vestedDelegationFixture); - - const positionBeforeTopUp = await rewardPool.delegationPositions( - this.delegatedValidators[0], - vestManager.address - ); - - const topUpAmount = (await rewardPool.delegationOf(this.delegatedValidators[0], vestManager.address)).mul(2); - await vestManager.topUpVestedDelegatePosition(this.delegatedValidators[0], { - value: topUpAmount.add(this.minDelegation), - }); - - const vestingEndAfter = (await rewardPool.delegationPositions(this.delegatedValidators[0], vestManager.address)) - .end; - expect(vestingEndAfter, "vestingEndAfter").to.be.eq(positionBeforeTopUp.end.add(positionBeforeTopUp.duration)); - }); - - it("should revert when top-up closed position", async function () { - const { validatorSet, rewardPool, liquidToken, vestManager } = await loadFixture( - this.fixtures.vestedDelegationFixture - ); - - // close position - const delegatedAmount = await rewardPool.delegationOf(this.delegatedValidators[0], vestManager.address); - await liquidToken.connect(this.vestManagerOwners[0]).approve(vestManager.address, delegatedAmount); - await vestManager.cutVestedDelegatePosition(this.delegatedValidators[0], delegatedAmount); - - // top-up - await expect( - vestManager.topUpVestedDelegatePosition(this.delegatedValidators[0], { value: this.minDelegation }) - ) - .to.be.revertedWithCustomError(validatorSet, "DelegateRequirement") - .withArgs("vesting", "POSITION_NOT_ACTIVE"); - }); - }); - describe("Reward Pool - rewards", async function () { RunVestedDelegationRewardsTests(); }); diff --git a/test/ValidatorSet/SwapVestedPositionValidator.test.ts b/test/ValidatorSet/SwapVestedPositionValidator.test.ts index fa428ae2..b24f4919 100644 --- a/test/ValidatorSet/SwapVestedPositionValidator.test.ts +++ b/test/ValidatorSet/SwapVestedPositionValidator.test.ts @@ -2,11 +2,11 @@ import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers"; import { expect } from "chai"; -import { DAY, WEEK } from "../constants"; +import { DAY, ERRORS, WEEK } from "../constants"; import { commitEpoch, commitEpochs, retrieveRPSData } from "../helper"; export function RunSwapVestedPositionValidatorTests(): void { - describe("Delegate position rewards", async function () { + describe.only("Delegate position rewards", async function () { it("should revert when not the vest manager owner", async function () { const { vestManager, delegatedValidator } = await loadFixture(this.fixtures.weeklyVestedDelegationFixture); @@ -17,7 +17,7 @@ export function RunSwapVestedPositionValidatorTests(): void { ).to.be.revertedWith("Ownable: caller is not the owner"); }); - it("should revert that the position is inactive", async function () { + it("should revert that the old position is inactive", async function () { const { systemValidatorSet, vestManager, delegatedValidator, rewardPool, vestManagerOwner } = await loadFixture( this.fixtures.weeklyVestedDelegationFixture ); @@ -44,7 +44,29 @@ export function RunSwapVestedPositionValidatorTests(): void { .withArgs("vesting", "OLD_POSITION_INACTIVE"); }); - it("should revert when we try to swap to validator with maturing position", async function () { + it("should revert when we try to swap to active position", async function () { + const { vestManager, liquidToken, vestManagerOwner, rewardPool } = await loadFixture( + this.fixtures.vestManagerFixture + ); + + const oldValidator = this.signers.validators[0]; + const newValidator = this.signers.validators[1]; + await liquidToken.connect(vestManagerOwner).approve(vestManager.address, this.minDelegation); + await vestManager + .connect(vestManagerOwner) + .openVestedDelegatePosition(oldValidator.address, 1, { value: this.minDelegation }); + await vestManager + .connect(vestManagerOwner) + .openVestedDelegatePosition(newValidator.address, 1, { value: this.minDelegation }); + + await expect( + vestManager.connect(vestManagerOwner).swapVestedPositionValidator(oldValidator.address, newValidator.address) + ) + .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") + .withArgs("vesting", ERRORS.swap.newPositionUnavilable); + }); + + it("should revert when we try to swap to new position which still matures", async function () { const { systemValidatorSet, vestManager, liquidToken, vestManagerOwner, rewardPool } = await loadFixture( this.fixtures.vestManagerFixture ); @@ -70,11 +92,11 @@ export function RunSwapVestedPositionValidatorTests(): void { vestManager.connect(vestManagerOwner).swapVestedPositionValidator(oldValidator.address, newValidator.address) ) .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") - .withArgs("vesting", "NEW_POSITION_MATURING"); + .withArgs("vesting", ERRORS.swap.newPositionUnavilable); }); - it("should revert when we try to swap to active position (balance > 0)", async function () { - const { vestManager, liquidToken, vestManagerOwner, rewardPool } = await loadFixture( + it("should revert when we try to swap to a position with left balance", async function () { + const { systemValidatorSet, vestManager, liquidToken, vestManagerOwner, rewardPool } = await loadFixture( this.fixtures.vestManagerFixture ); @@ -83,16 +105,74 @@ export function RunSwapVestedPositionValidatorTests(): void { await liquidToken.connect(vestManagerOwner).approve(vestManager.address, this.minDelegation); await vestManager .connect(vestManagerOwner) - .openVestedDelegatePosition(oldValidator.address, 1, { value: this.minDelegation }); + .openVestedDelegatePosition(oldValidator.address, 5, { value: this.minDelegation }); + await vestManager + .connect(vestManagerOwner) + .openVestedDelegatePosition(newValidator.address, 1, { value: this.minDelegation }); + + // commit 5 epochs with 3 days increase before each, so, the new position will be matured and have some balance left + await commitEpochs(systemValidatorSet, rewardPool, [oldValidator, newValidator], 5, this.epochSize, DAY * 3); + + // prepare params for call + const { epochNum, topUpIndex } = await retrieveRPSData( + systemValidatorSet, + rewardPool, + newValidator.address, + vestManager.address + ); + + // claim rewards only + await vestManager.connect(vestManagerOwner).claimVestedPositionReward(newValidator.address, epochNum, topUpIndex); + + // verify that there is delegated balance left + expect(await rewardPool.delegationOf(newValidator.address, vestManager.address), "delegationOf").to.not.be.eq(0); + + await expect( + vestManager.connect(vestManagerOwner).swapVestedPositionValidator(oldValidator.address, newValidator.address) + ) + .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") + .withArgs("vesting", ERRORS.swap.newPositionUnavilable); + }); + + it("should revert when we try to swap to a position with left rewards to claim", async function () { + const { systemValidatorSet, vestManager, liquidToken, vestManagerOwner, rewardPool } = await loadFixture( + this.fixtures.vestManagerFixture + ); + + const oldValidator = this.signers.validators[0]; + const newValidator = this.signers.validators[1]; + await liquidToken.connect(vestManagerOwner).approve(vestManager.address, this.minDelegation); + await vestManager + .connect(vestManagerOwner) + .openVestedDelegatePosition(oldValidator.address, 5, { value: this.minDelegation }); await vestManager .connect(vestManagerOwner) .openVestedDelegatePosition(newValidator.address, 1, { value: this.minDelegation }); + // commit 5 epochs with 3 days increase before each, so, the new position will be matured and have some balance left + await commitEpochs(systemValidatorSet, rewardPool, [oldValidator, newValidator], 5, this.epochSize, DAY * 3); + + // undelegate full amount + await vestManager.connect(vestManagerOwner).cutVestedDelegatePosition(newValidator.address, this.minDelegation); + + // verify that there are rewards left to claim + const { epochNum, topUpIndex } = await retrieveRPSData( + systemValidatorSet, + rewardPool, + newValidator.address, + vestManager.address + ); + + expect( + await rewardPool.getDelegatorPositionReward(newValidator.address, vestManager.address, epochNum, topUpIndex), + "getDelegatorPositionReward" + ).to.not.be.eq(0); + await expect( vestManager.connect(vestManagerOwner).swapVestedPositionValidator(oldValidator.address, newValidator.address) ) .to.be.revertedWithCustomError(rewardPool, "DelegateRequirement") - .withArgs("vesting", "INVALID_NEW_POSITION"); + .withArgs("vesting", ERRORS.swap.newPositionUnavilable); }); it("should transfer old position parameters to the new one on successful swap", async function () { @@ -245,7 +325,8 @@ export function RunSwapVestedPositionValidatorTests(): void { .withArgs("_saveAccountParamsChange", "BALANCE_CHANGE_ALREADY_MADE"); }); - it("should revert when try to swap too many times", async function () { + // TODO: Consider deleting it as we shouldn't be getting into that case + it.skip("should revert when try to swap too many times", async function () { const { systemValidatorSet, rewardPool, diff --git a/test/constants.ts b/test/constants.ts index ffe6ccd3..43e0dbcd 100644 --- a/test/constants.ts +++ b/test/constants.ts @@ -39,6 +39,9 @@ export const ERRORS = { ownable: "Ownable: caller is not the owner", inactiveValidator: "INACTIVE_VALIDATOR", invalidValidator: "INVALID_VALIDATOR", + swap: { + newPositionUnavilable: "NEW_POSITION_UNAVAILABLE", + }, accessControl: (account: string, role: string) => { return `AccessControl: account ${account} is missing role ${role}`; }, From 4cb12ce9598d4a4252ac88c643783e06ce8b645f Mon Sep 17 00:00:00 2001 From: Vitomir Pavlov Date: Thu, 6 Jun 2024 15:35:15 +0300 Subject: [PATCH 7/7] delete only --- test/ValidatorSet/SwapVestedPositionValidator.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ValidatorSet/SwapVestedPositionValidator.test.ts b/test/ValidatorSet/SwapVestedPositionValidator.test.ts index b24f4919..35ac8de4 100644 --- a/test/ValidatorSet/SwapVestedPositionValidator.test.ts +++ b/test/ValidatorSet/SwapVestedPositionValidator.test.ts @@ -6,7 +6,7 @@ import { DAY, ERRORS, WEEK } from "../constants"; import { commitEpoch, commitEpochs, retrieveRPSData } from "../helper"; export function RunSwapVestedPositionValidatorTests(): void { - describe.only("Delegate position rewards", async function () { + describe("Delegate position rewards", async function () { it("should revert when not the vest manager owner", async function () { const { vestManager, delegatedValidator } = await loadFixture(this.fixtures.weeklyVestedDelegationFixture);