From 6a69f9c9fcb734637b5588b280c50968308705ec Mon Sep 17 00:00:00 2001 From: Alain Olivier Date: Thu, 25 Jan 2024 16:43:40 +0100 Subject: [PATCH 01/15] feat(contracts): add RewardsManager contract --- contracts/core/RewardsManager.sol | 123 +++++++++++++++++++++++ contracts/interfaces/IRewardsManager.sol | 15 +++ contracts/libraries/Errors.sol | 2 + contracts/libraries/Roles.sol | 1 + 4 files changed, 141 insertions(+) create mode 100644 contracts/core/RewardsManager.sol create mode 100644 contracts/interfaces/IRewardsManager.sol diff --git a/contracts/core/RewardsManager.sol b/contracts/core/RewardsManager.sol new file mode 100644 index 0000000..f98b190 --- /dev/null +++ b/contracts/core/RewardsManager.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/interfaces/IERC20Upgradeable.sol"; +import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {IDandelionVoting} from "../interfaces/external/IDandelionVoting.sol"; +import {ITokenManager} from "../interfaces/external/ITokenManager.sol"; +import {IRewardsManager} from "../interfaces/IRewardsManager.sol"; +import {IEpochsManager} from "../interfaces/IEpochsManager.sol"; +import {Errors} from "../libraries/Errors.sol"; +import {Roles} from "../libraries/Roles.sol"; + +contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, AccessControlEnumerableUpgradeable { + using SafeERC20Upgradeable for IERC20Upgradeable; + address public epochsManager; + address public dandelionVoting; + uint256 public maxTotalSupply; + address public token; + address public tokenManager; + + mapping(uint16 => uint256) depositedAmountByEpoch; + mapping(uint16 => mapping(address => uint256)) lockedRewardByEpoch; + + function initialize( + address _epochsManager, + address _dandelionVoting, + address _token, + address _tokenManager, + uint256 _maxTotalSupply + ) public initializer { + __UUPSUpgradeable_init(); + __AccessControlEnumerable_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); + + epochsManager = _epochsManager; + dandelionVoting = _dandelionVoting; + token = _token; + tokenManager = _tokenManager; + maxTotalSupply = _maxTotalSupply; + } + + function claimReward(uint16 epoch) external { + address sender = _msgSender(); + uint16 currentEpoch = IEpochsManager(epochsManager).currentEpoch(); + if (currentEpoch - epoch < 12) revert Errors.TooEarly(); + uint256 amount = lockedRewardByEpoch[epoch][sender]; + if (amount > 0) { + ITokenManager(tokenManager).burn(sender, amount); + IERC20Upgradeable(token).safeTransfer(sender, amount); + delete lockedRewardByEpoch[epoch][sender]; + } + } + + function depositForEpoch(uint16 epoch, uint256 amount) external onlyRole(Roles.DEPOSIT_REWARD_ROLE) { + address sender = _msgSender(); + IERC20Upgradeable(token).safeTransferFrom(sender, address(this), amount); + depositedAmountByEpoch[epoch] += amount; + } + + function registerRewards(uint16 epoch, address[] calldata stakers) external { + uint16 currentEpoch = IEpochsManager(epochsManager).currentEpoch(); + if (epoch >= currentEpoch) revert Errors.InvalidEpoch(); + for (uint256 i = 0; i < stakers.length; ) { + if (lockedRewardByEpoch[epoch][stakers[i]] > 0) continue; + if (!_hasVotedInEpoch(epoch, stakers[i])) continue; + uint256 amount = _calculateRewardForEpoch(epoch, stakers[i]); + ITokenManager(tokenManager).mint(stakers[i], amount); + _checkTotalSupply(); + lockedRewardByEpoch[epoch][stakers[i]] = amount; + } + } + + function _authorizeUpgrade(address) internal override onlyRole(Roles.UPGRADE_ROLE) {} + + function _calculateRewardForEpoch(uint16, address) private pure returns (uint256) { + return 1; + } + + function _checkTotalSupply() internal { + address minime = ITokenManager(tokenManager).token(); + if (IERC20Upgradeable(minime).totalSupply() > maxTotalSupply) { + revert Errors.MaxTotalSupplyExceeded(); + } + } + + function _hasVotedInEpoch(uint16 epoch, address staker) private returns (bool) { + address dandelionVotingAddress = dandelionVoting; + uint256 numberOfVotes = IDandelionVoting(dandelionVotingAddress).votesLength(); + uint64 voteDuration = IDandelionVoting(dandelionVotingAddress).duration(); + + uint256 epochDuration = IEpochsManager(epochsManager).epochDuration(); + uint256 startFirstEpochTimestamp = IEpochsManager(epochsManager).startFirstEpochTimestamp(); + + uint256 epochStartDate = startFirstEpochTimestamp + (epoch * epochDuration); + uint256 epochEndDate = epochStartDate + epochDuration - 1; + + for (uint256 voteId = numberOfVotes; voteId >= 1; ) { + (, , uint64 voteStartDate, , , , , , , , ) = IDandelionVoting(dandelionVotingAddress).getVote(voteId); + + uint64 voteEndDate = voteStartDate + voteDuration; + if (voteEndDate >= epochStartDate && voteEndDate <= epochEndDate) { + if ( + IDandelionVoting(dandelionVotingAddress).getVoterState(voteId, staker) != + IDandelionVoting.VoterState.Absent + ) { + unchecked { + return true; + } + } + } + unchecked { + --voteId; + } + } + + return false; + } +} diff --git a/contracts/interfaces/IRewardsManager.sol b/contracts/interfaces/IRewardsManager.sol new file mode 100644 index 0000000..90feabc --- /dev/null +++ b/contracts/interfaces/IRewardsManager.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +/** + * @title IRewardsManager + * @author pNetwork + * + * @notice + */ +interface IRewardsManager { + function registerRewards(uint16 epoch, address[] calldata stakers) external; + + function claimReward(uint16 epoch) external; +} diff --git a/contracts/libraries/Errors.sol b/contracts/libraries/Errors.sol index 93a9d1a..dedafb3 100644 --- a/contracts/libraries/Errors.sol +++ b/contracts/libraries/Errors.sol @@ -22,4 +22,6 @@ library Errors { error InvalidSignatureNonce(uint256 signatureNonce, uint256 expectedSignatureNonce); error ActorAlreadySlashed(uint256 lastSlashTimestamp, uint256 slashTimestamp); error ActorAlreadyResumed(uint256 lastResumeTimestamp, uint256 slashTimestamp); + error TooEarly(); + error AlreadyDeposited(); } diff --git a/contracts/libraries/Roles.sol b/contracts/libraries/Roles.sol index 98c7bf6..05865b2 100644 --- a/contracts/libraries/Roles.sol +++ b/contracts/libraries/Roles.sol @@ -20,4 +20,5 @@ library Roles { bytes32 public constant SET_FEES_MANAGER_ROLE = keccak256("SET_FEES_MANAGER_ROLE"); bytes32 public constant SET_GOVERNANCE_MESSAGE_EMITTER_ROLE = keccak256("SET_GOVERNANCE_MESSAGE_EMITTER_ROLE"); bytes32 public constant INCREASE_AMOUNT_ROLE = keccak256("INCREASE_AMOUNT_ROLE"); + bytes32 public constant DEPOSIT_REWARD_ROLE = keccak256("DEPOSIT_REWARD_ROLE"); } From ece456bd7306883407912cec475d7445a3157868 Mon Sep 17 00:00:00 2001 From: Alain Olivier Date: Fri, 26 Jan 2024 11:42:26 +0100 Subject: [PATCH 02/15] test: add basic tests for RewardsManager --- test/RewardsManager.test.js | 160 ++++++++++++++++++++++++++++++++++++ test/constants.js | 3 + test/roles.js | 3 +- 3 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 test/RewardsManager.test.js diff --git a/test/RewardsManager.test.js b/test/RewardsManager.test.js new file mode 100644 index 0000000..cedb82d --- /dev/null +++ b/test/RewardsManager.test.js @@ -0,0 +1,160 @@ +const { time } = require('@nomicfoundation/hardhat-network-helpers') +const { expect } = require('chai') +const { ethers, upgrades, config, network } = require('hardhat') +const R = require('ramda') + +const AclAbi = require('./abi/ACL.json') +const { EPOCH_DURATION, TOKEN_MANAGER_ADDRESS, ONE_DAY, ONE_MONTH, DAO_CREATOR, ACL_ADDRESS } = require('./constants') +const { DEPOSIT_REWARD_ROLE, MINT_ROLE, BURN_ROLE } = require('./roles') + +describe('RewardsManager', () => { + let epochsManager, + pnt, + owner, + pntHolder1, + pntHolder2, + pntHolder3, + pntHolder4, + pntHolders, + randomGuy, + dandelionVoting, + rewardsManager, + acl, + daoCreator + + const setPermission = async (entity, app, role) => acl.connect(daoCreator).grantPermission(entity, app, role) + + const sendEthers = (_from, _dest, _amount) => + _from.sendTransaction({ + to: _dest.address, + value: ethers.utils.parseEther(_amount) + }) + + const sendPnt = (_from, _to, _amount) => pnt.connect(_from).transfer(_to.address, ethers.utils.parseEther(_amount)) + + const missingSteps = async () => { + await setPermission(await rewardsManager.getAddress(), TOKEN_MANAGER_ADDRESS, MINT_ROLE) + await setPermission(await rewardsManager.getAddress(), TOKEN_MANAGER_ADDRESS, BURN_ROLE) + } + + beforeEach(async () => { + await network.provider.request({ + method: 'hardhat_reset', + params: [ + { + forking: { + jsonRpcUrl: config.networks.hardhat.forking.url + } + } + ] + }) + + const RewardsManager = await ethers.getContractFactory('RewardsManager') + const EpochsManager = await ethers.getContractFactory('EpochsManager') + const TestToken = await ethers.getContractFactory('TestToken') + const MockDandelionVotingContract = await ethers.getContractFactory('MockDandelionVotingContract') + + const signers = await ethers.getSigners() + owner = signers[0] + daoCreator = await ethers.getImpersonatedSigner(DAO_CREATOR) + randomGuy = ethers.Wallet.createRandom().connect(ethers.provider) + pntHolder1 = ethers.Wallet.createRandom().connect(ethers.provider) + pntHolder2 = ethers.Wallet.createRandom().connect(ethers.provider) + pntHolder3 = ethers.Wallet.createRandom().connect(ethers.provider) + pntHolder4 = ethers.Wallet.createRandom().connect(ethers.provider) + pntHolders = [pntHolder1, pntHolder2, pntHolder3, pntHolder4] + + await Promise.all([...pntHolders, randomGuy].map((_dest) => sendEthers(owner, _dest, '1'))) + + acl = await ethers.getContractAt(AclAbi, ACL_ADDRESS) + pnt = await TestToken.deploy('PNT', 'PNT') + + await Promise.all(pntHolders.map((_holder) => sendPnt(owner, _holder, '400000'))) + + dandelionVoting = await MockDandelionVotingContract.deploy() + await dandelionVoting.setTestStartDate(EPOCH_DURATION * 1000) // this is needed to don't break normal tests + + epochsManager = await upgrades.deployProxy(EpochsManager, [EPOCH_DURATION, 0], { + initializer: 'initialize', + kind: 'uups' + }) + + rewardsManager = await upgrades.deployProxy( + RewardsManager, + [epochsManager.address, await dandelionVoting.getAddress(), await pnt.getAddress(), TOKEN_MANAGER_ADDRESS, 100], + { + initializer: 'initialize', + kind: 'uups' + } + ) + + await missingSteps() + }) + + it('should deploy correctly', async () => { + expect(await rewardsManager.token()).to.eq(await pnt.getAddress()) + expect(await rewardsManager.tokenManager()).to.eq(TOKEN_MANAGER_ADDRESS) + }) + + it('should be possible to deposit tokens', async () => { + const amount = 100 + expect(await epochsManager.currentEpoch()).to.be.eq(0) + await rewardsManager.grantRole(DEPOSIT_REWARD_ROLE, owner.address) + await pnt.approve(await rewardsManager.getAddress(), amount) + const pntOwnerBalancePre = await pnt.balanceOf(owner.address) + const pntRewardsManagerBalancePre = await pnt.balanceOf(await rewardsManager.getAddress()) + await rewardsManager.depositForEpoch(0, amount) + const pntOwnerBalancePost = await pnt.balanceOf(owner.address) + const pntRewardsManagerBalancePost = await pnt.balanceOf(await rewardsManager.getAddress()) + expect(pntOwnerBalancePost).to.be.eq(pntOwnerBalancePre.sub(amount)) + expect(pntRewardsManagerBalancePost).to.be.eq(pntRewardsManagerBalancePre.add(amount)) + expect(await rewardsManager.depositedAmountByEpoch(0)).to.be.eq(amount) + }) + + it('should not be able to register for rewards without voting', async () => { + const amount = 100 + expect(await epochsManager.currentEpoch()).to.be.eq(0) + await rewardsManager.grantRole(DEPOSIT_REWARD_ROLE, owner.address) + await pnt.approve(await rewardsManager.getAddress(), amount) + const pntOwnerBalancePre = await pnt.balanceOf(owner.address) + const pntRewardsManagerBalancePre = await pnt.balanceOf(await rewardsManager.getAddress()) + await rewardsManager.depositForEpoch(0, amount) + const pntOwnerBalancePost = await pnt.balanceOf(owner.address) + const pntRewardsManagerBalancePost = await pnt.balanceOf(await rewardsManager.getAddress()) + expect(pntOwnerBalancePost).to.be.eq(pntOwnerBalancePre.sub(amount)) + expect(pntRewardsManagerBalancePost).to.be.eq(pntRewardsManagerBalancePre.add(amount)) + await time.increase(ONE_DAY) + await dandelionVoting.setTestStartDate(await time.latest()) + await Promise.all( + R.zip(pntHolders, [1, 1, 2, 0]).map(([holder, status]) => + dandelionVoting.setTestVoteState(holder.address, status) + ) + ) + await time.increase(ONE_MONTH + ONE_DAY) + await expect( + rewardsManager.connect(randomGuy).registerRewards(0, [pntHolder1.address, pntHolder2.address, pntHolder3.address]) + ).to.not.be.reverted + await expect(rewardsManager.connect(randomGuy).registerRewards(0, [pntHolder3.address, pntHolder4.address])).to.not + .be.reverted + expect(await rewardsManager.lockedRewardByEpoch(0, pntHolder1.address)).to.be.eq(1) + expect(await rewardsManager.lockedRewardByEpoch(0, pntHolder2.address)).to.be.eq(1) + expect(await rewardsManager.lockedRewardByEpoch(0, pntHolder3.address)).to.be.eq(1) + expect(await rewardsManager.lockedRewardByEpoch(0, pntHolder4.address)).to.be.eq(0) + await expect(rewardsManager.connect(pntHolder1).claimRewardByEpoch(0)).to.be.revertedWithCustomError( + rewardsManager, + 'TooEarly' + ) + await time.increase(ONE_MONTH * 12 + ONE_DAY) + await expect(rewardsManager.connect(pntHolder1).claimRewardByEpoch(0)) + .to.emit(pnt, 'Transfer') + .withArgs(await rewardsManager.getAddress(), pntHolder1.address, 1) + await time.increase(ONE_MONTH) + await expect(rewardsManager.connect(pntHolder2).claimRewardByEpoch(0)) + .to.emit(pnt, 'Transfer') + .withArgs(await rewardsManager.getAddress(), pntHolder2.address, 1) + await expect(rewardsManager.connect(randomGuy).claimRewardByEpoch(0)).to.be.revertedWithCustomError( + rewardsManager, + 'NothingToClaim' + ) + }) +}) diff --git a/test/constants.js b/test/constants.js index 5efa719..db6b1a3 100644 --- a/test/constants.js +++ b/test/constants.js @@ -12,6 +12,7 @@ module.exports = { MINIMUM_BORROWING_FEE: 0.3 * 10 ** 6, // 30% ONE_DAY: 86400, + ONE_MONTH: 86400 * 30, PBTC_ADDRESS: '0x62199B909FB8B8cf870f97BEf2cE6783493c4908', PNETWORK_ADDRESS: '0x341aA660fD5c280F5a9501E3822bB4a98E816D1b', PNETWORK_NETWORK_IDS: { @@ -32,6 +33,8 @@ module.exports = { REGISTRATION_SENTINEL_STAKING: '0x01', REGISTRATON_GUARDIAN: '0x03', TOKEN_MANAGER_ADDRESS: '0xCec0058735D50de98d3715792569921FEb9EfDC1', + DAO_CREATOR: '0x08544a580EDC2C3F27689b740F8257A29166FC77', + ACL_CONTRACT: '0x50b2b8e429cB51bD43cD3E690e5BEB9eb674f6d7', ZERO_ADDRESS: '0x0000000000000000000000000000000000000000', ONE_HOUR_IN_S: 3600 } diff --git a/test/roles.js b/test/roles.js index 267fbee..e1cb075 100644 --- a/test/roles.js +++ b/test/roles.js @@ -23,5 +23,6 @@ module.exports = { STAKE_ROLE: getRole('STAKE_ROLE'), TRANSFER_ROLE: getRole('TRANSFER_ROLE'), UPDATE_GUARDIAN_REGISTRATION_ROLE: getRole('UPDATE_GUARDIAN_REGISTRATION_ROLE'), - UPGRADE_ROLE: getRole('UPGRADE_ROLE') + UPGRADE_ROLE: getRole('UPGRADE_ROLE'), + DEPOSIT_REWARD_ROLE: getRole('DEPOSIT_REWARD_ROLE') } From 9743e307c2edd917c36af7ce5bfb64244f6aa9c1 Mon Sep 17 00:00:00 2001 From: Alain Olivier Date: Fri, 26 Jan 2024 15:56:57 +0100 Subject: [PATCH 03/15] refactor(contracts): refactor RewardsManager --- contracts/core/RewardsManager.sol | 87 +- contracts/interfaces/IRewardsManager.sol | 6 +- .../interfaces/external/IDandelionVoting.sol | 14 +- .../interfaces/external/IMinimeToken.sol | 11 + contracts/libraries/Errors.sol | 2 + contracts/libraries/Roles.sol | 1 + contracts/test/MockDandelionVoting.sol | 18 +- test/RewardsManager.test.js | 91 ++- test/abi/TokenManager.json | 755 ++++++++++++++++++ 9 files changed, 918 insertions(+), 67 deletions(-) create mode 100644 contracts/interfaces/external/IMinimeToken.sol create mode 100644 test/abi/TokenManager.json diff --git a/contracts/core/RewardsManager.sol b/contracts/core/RewardsManager.sol index f98b190..5e2d2e2 100644 --- a/contracts/core/RewardsManager.sol +++ b/contracts/core/RewardsManager.sol @@ -8,6 +8,7 @@ import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ER import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {IDandelionVoting} from "../interfaces/external/IDandelionVoting.sol"; +import {IMinimeToken} from "../interfaces/external/IMinimeToken.sol"; import {ITokenManager} from "../interfaces/external/ITokenManager.sol"; import {IRewardsManager} from "../interfaces/IRewardsManager.sol"; import {IEpochsManager} from "../interfaces/IEpochsManager.sol"; @@ -22,8 +23,12 @@ contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, Acce address public token; address public tokenManager; - mapping(uint16 => uint256) depositedAmountByEpoch; - mapping(uint16 => mapping(address => uint256)) lockedRewardByEpoch; + mapping(uint16 => uint256) public depositedAmountByEpoch; + mapping(uint16 => uint256) public claimedAmountByEpoch; + mapping(uint16 => uint256) public unclaimableAmountByEpoch; + mapping(uint16 => mapping(address => uint256)) public lockedRewardByEpoch; + + event RewardRegistered(uint16 indexed epoch, address indexed staker, uint256 amount); function initialize( address _epochsManager, @@ -44,7 +49,7 @@ contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, Acce maxTotalSupply = _maxTotalSupply; } - function claimReward(uint16 epoch) external { + function claimRewardByEpoch(uint16 epoch) external { address sender = _msgSender(); uint16 currentEpoch = IEpochsManager(epochsManager).currentEpoch(); if (currentEpoch - epoch < 12) revert Errors.TooEarly(); @@ -53,32 +58,52 @@ contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, Acce ITokenManager(tokenManager).burn(sender, amount); IERC20Upgradeable(token).safeTransfer(sender, amount); delete lockedRewardByEpoch[epoch][sender]; - } + claimedAmountByEpoch[epoch] += amount; + } else revert Errors.NothingToClaim(); } function depositForEpoch(uint16 epoch, uint256 amount) external onlyRole(Roles.DEPOSIT_REWARD_ROLE) { address sender = _msgSender(); + uint16 currentEpoch = IEpochsManager(epochsManager).currentEpoch(); + if (epoch < currentEpoch) revert Errors.InvalidEpoch(); IERC20Upgradeable(token).safeTransferFrom(sender, address(this), amount); depositedAmountByEpoch[epoch] += amount; } - function registerRewards(uint16 epoch, address[] calldata stakers) external { + function registerRewardsForEpoch(uint16 epoch, address[] calldata stakers) external { uint16 currentEpoch = IEpochsManager(epochsManager).currentEpoch(); if (epoch >= currentEpoch) revert Errors.InvalidEpoch(); - for (uint256 i = 0; i < stakers.length; ) { + for (uint256 i = 0; i < stakers.length; i++) { if (lockedRewardByEpoch[epoch][stakers[i]] > 0) continue; - if (!_hasVotedInEpoch(epoch, stakers[i])) continue; - uint256 amount = _calculateRewardForEpoch(epoch, stakers[i]); - ITokenManager(tokenManager).mint(stakers[i], amount); - _checkTotalSupply(); - lockedRewardByEpoch[epoch][stakers[i]] = amount; + bool hasVoted = _hasVotedInEpoch(epoch, stakers[i]); + uint256 amount = _calculateStakerRewardForEpoch(epoch, stakers[i]); + if (hasVoted && amount > 0) { + ITokenManager(tokenManager).mint(stakers[i], amount); + _checkTotalSupply(); + lockedRewardByEpoch[epoch][stakers[i]] = amount; + emit RewardRegistered(epoch, stakers[i], amount); + } else if (amount > 0) { + unclaimableAmountByEpoch[epoch] += amount; + } } } + function withdrawUnclaimableRewardsForEpoch(uint16 epoch) external onlyRole(Roles.WITHDRAW_ROLE) { + if (unclaimableAmountByEpoch[epoch] > 0) { + address sender = _msgSender(); + IERC20Upgradeable(token).safeTransfer(sender, unclaimableAmountByEpoch[epoch]); + delete unclaimableAmountByEpoch[epoch]; + } else revert Errors.NothingToWithdraw(); + } + function _authorizeUpgrade(address) internal override onlyRole(Roles.UPGRADE_ROLE) {} - function _calculateRewardForEpoch(uint16, address) private pure returns (uint256) { - return 1; + function _calculateStakerRewardForEpoch(uint16 epoch, address staker) private returns (uint256) { + address minime = ITokenManager(tokenManager).token(); + (, , , uint64 snapshotBlock) = _getLastVoteInEpoch(epoch); + uint256 supply = IMinimeToken(minime).totalSupplyAt(snapshotBlock); + uint256 balance = IMinimeToken(minime).balanceOfAt(staker, snapshotBlock); + return (depositedAmountByEpoch[epoch] * balance) / supply; } function _checkTotalSupply() internal { @@ -88,24 +113,44 @@ contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, Acce } } - function _hasVotedInEpoch(uint16 epoch, address staker) private returns (bool) { - address dandelionVotingAddress = dandelionVoting; - uint256 numberOfVotes = IDandelionVoting(dandelionVotingAddress).votesLength(); - uint64 voteDuration = IDandelionVoting(dandelionVotingAddress).duration(); - + function _getEpochTimestamps(uint16 epoch) private view returns (uint256, uint256) { uint256 epochDuration = IEpochsManager(epochsManager).epochDuration(); uint256 startFirstEpochTimestamp = IEpochsManager(epochsManager).startFirstEpochTimestamp(); - uint256 epochStartDate = startFirstEpochTimestamp + (epoch * epochDuration); uint256 epochEndDate = epochStartDate + epochDuration - 1; + return (epochStartDate, epochEndDate); + } + function _getLastVoteInEpoch(uint16 epoch) private returns (uint256, uint64, uint64, uint64) { + uint256 numberOfVotes = IDandelionVoting(dandelionVoting).votesLength(); + uint64 voteDuration = IDandelionVoting(dandelionVoting).duration(); + (uint256 epochStartDate, uint256 epochEndDate) = _getEpochTimestamps(epoch); + for (uint256 voteId = numberOfVotes; voteId >= 1; ) { + (, , uint64 startDate, uint64 executionDate, uint64 snapshotBlock, , , , , , ) = IDandelionVoting( + dandelionVoting + ).getVote(voteId); + uint64 voteEndDate = startDate + voteDuration; + if (voteEndDate >= epochStartDate && voteEndDate <= epochEndDate) { + return (voteId, startDate, executionDate, snapshotBlock); + } + unchecked { + --voteId; + } + } + revert Errors.NoVoteInEpoch(); + } + + function _hasVotedInEpoch(uint16 epoch, address staker) private returns (bool) { + uint256 numberOfVotes = IDandelionVoting(dandelionVoting).votesLength(); + uint64 voteDuration = IDandelionVoting(dandelionVoting).duration(); + (uint256 epochStartDate, uint256 epochEndDate) = _getEpochTimestamps(epoch); for (uint256 voteId = numberOfVotes; voteId >= 1; ) { - (, , uint64 voteStartDate, , , , , , , , ) = IDandelionVoting(dandelionVotingAddress).getVote(voteId); + (, , uint64 voteStartDate, , , , , , , , ) = IDandelionVoting(dandelionVoting).getVote(voteId); uint64 voteEndDate = voteStartDate + voteDuration; if (voteEndDate >= epochStartDate && voteEndDate <= epochEndDate) { if ( - IDandelionVoting(dandelionVotingAddress).getVoterState(voteId, staker) != + IDandelionVoting(dandelionVoting).getVoterState(voteId, staker) != IDandelionVoting.VoterState.Absent ) { unchecked { diff --git a/contracts/interfaces/IRewardsManager.sol b/contracts/interfaces/IRewardsManager.sol index 90feabc..eead272 100644 --- a/contracts/interfaces/IRewardsManager.sol +++ b/contracts/interfaces/IRewardsManager.sol @@ -9,7 +9,9 @@ pragma solidity ^0.8.17; * @notice */ interface IRewardsManager { - function registerRewards(uint16 epoch, address[] calldata stakers) external; + function claimRewardByEpoch(uint16 epoch) external; - function claimReward(uint16 epoch) external; + function depositForEpoch(uint16 epoch, uint256 amount) external; + + function registerRewardsForEpoch(uint16 epoch, address[] calldata stakers) external; } diff --git a/contracts/interfaces/external/IDandelionVoting.sol b/contracts/interfaces/external/IDandelionVoting.sol index 10bc12e..c07aca4 100644 --- a/contracts/interfaces/external/IDandelionVoting.sol +++ b/contracts/interfaces/external/IDandelionVoting.sol @@ -13,7 +13,19 @@ interface IDandelionVoting { function getVote( uint256 voteId - ) external returns (bool, bool, uint64, uint64, uint64, uint64, uint64, uint256, uint256, uint256, bytes memory); + ) external returns ( + bool open, + bool executed, + uint64 startDate, + uint64 executionDate, + uint64 snapshotBlock, + uint64 supportRequired, + uint64 minAcceptQuorum, + uint256 votingPower, + uint256 yea, + uint256 nay, + bytes memory script + ); function getVoterState(uint256 voteId, address beneficiary) external returns (VoterState); diff --git a/contracts/interfaces/external/IMinimeToken.sol b/contracts/interfaces/external/IMinimeToken.sol new file mode 100644 index 0000000..eca6072 --- /dev/null +++ b/contracts/interfaces/external/IMinimeToken.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IMinimeToken is IERC20 { + function balanceOfAt(address _owner, uint _blockNumber) external returns (uint); + + function totalSupplyAt(uint _blockNumber) external returns (uint); +} diff --git a/contracts/libraries/Errors.sol b/contracts/libraries/Errors.sol index dedafb3..364b9c7 100644 --- a/contracts/libraries/Errors.sol +++ b/contracts/libraries/Errors.sol @@ -24,4 +24,6 @@ library Errors { error ActorAlreadyResumed(uint256 lastResumeTimestamp, uint256 slashTimestamp); error TooEarly(); error AlreadyDeposited(); + error NoVoteInEpoch(); + error NothingToWithdraw(); } diff --git a/contracts/libraries/Roles.sol b/contracts/libraries/Roles.sol index 05865b2..0959a69 100644 --- a/contracts/libraries/Roles.sol +++ b/contracts/libraries/Roles.sol @@ -21,4 +21,5 @@ library Roles { bytes32 public constant SET_GOVERNANCE_MESSAGE_EMITTER_ROLE = keccak256("SET_GOVERNANCE_MESSAGE_EMITTER_ROLE"); bytes32 public constant INCREASE_AMOUNT_ROLE = keccak256("INCREASE_AMOUNT_ROLE"); bytes32 public constant DEPOSIT_REWARD_ROLE = keccak256("DEPOSIT_REWARD_ROLE"); + bytes32 public constant WITHDRAW_ROLE = keccak256("WITHDRAW_ROLE"); } diff --git a/contracts/test/MockDandelionVoting.sol b/contracts/test/MockDandelionVoting.sol index 611d272..12ba5d3 100644 --- a/contracts/test/MockDandelionVoting.sol +++ b/contracts/test/MockDandelionVoting.sol @@ -3,14 +3,16 @@ pragma solidity ^0.8.17; contract MockDandelionVotingContract { uint64 private _testStartDate; - uint256 private _testVoteState; + uint64 private _testSnapshotBlock; + mapping(address => uint256) private _testVoteState; - function setTestVoteState(uint256 testVoteState_) external { - _testVoteState = testVoteState_; + function setTestVoteState(address voter, uint256 testVoteState_) external { + _testVoteState[voter] = testVoteState_; } - function setTestStartDate(uint64 testStartDate_) external { - _testStartDate = testStartDate_; + function setTestStartDate() external { + _testStartDate = uint64(block.timestamp); + _testSnapshotBlock = uint64(block.number); } function votesLength() external pure returns (uint256) { @@ -44,7 +46,7 @@ contract MockDandelionVotingContract { executed = true; startDate = _testStartDate; executionDate = _testStartDate + duration(); - snapshotBlock = 0; + snapshotBlock = _testSnapshotBlock; votingPower = 0; supportRequired = 0; minAcceptQuorum = 0; @@ -53,7 +55,7 @@ contract MockDandelionVotingContract { script = ""; } - function getVoterState(uint256, address) external view returns (uint256) { - return _testVoteState; + function getVoterState(uint256, address voter) external view returns (uint256) { + return _testVoteState[voter]; } } diff --git a/test/RewardsManager.test.js b/test/RewardsManager.test.js index cedb82d..82b6c3b 100644 --- a/test/RewardsManager.test.js +++ b/test/RewardsManager.test.js @@ -4,8 +4,18 @@ const { ethers, upgrades, config, network } = require('hardhat') const R = require('ramda') const AclAbi = require('./abi/ACL.json') -const { EPOCH_DURATION, TOKEN_MANAGER_ADDRESS, ONE_DAY, ONE_MONTH, DAO_CREATOR, ACL_ADDRESS } = require('./constants') +const TokenManagerAbi = require('./abi/TokenManager.json') +const { + EPOCH_DURATION, + TOKEN_MANAGER_ADDRESS, + ONE_DAY, + ONE_MONTH, + DAO_CREATOR, + ACL_ADDRESS, + PNT_MAX_TOTAL_SUPPLY +} = require('./constants') const { DEPOSIT_REWARD_ROLE, MINT_ROLE, BURN_ROLE } = require('./roles') +const { hardhatReset } = require('./utils/hardhat-reset') describe('RewardsManager', () => { let epochsManager, @@ -20,6 +30,7 @@ describe('RewardsManager', () => { dandelionVoting, rewardsManager, acl, + tokenManager, daoCreator const setPermission = async (entity, app, role) => acl.connect(daoCreator).grantPermission(entity, app, role) @@ -27,27 +38,20 @@ describe('RewardsManager', () => { const sendEthers = (_from, _dest, _amount) => _from.sendTransaction({ to: _dest.address, - value: ethers.utils.parseEther(_amount) + value: ethers.parseEther(_amount) }) - const sendPnt = (_from, _to, _amount) => pnt.connect(_from).transfer(_to.address, ethers.utils.parseEther(_amount)) + const sendPnt = (_from, _to, _amount) => pnt.connect(_from).transfer(_to, ethers.parseEther(_amount)) const missingSteps = async () => { - await setPermission(await rewardsManager.getAddress(), TOKEN_MANAGER_ADDRESS, MINT_ROLE) - await setPermission(await rewardsManager.getAddress(), TOKEN_MANAGER_ADDRESS, BURN_ROLE) + await setPermission(await rewardsManager.getAddress(), await tokenManager.getAddress(), MINT_ROLE) + await setPermission(await rewardsManager.getAddress(), await tokenManager.getAddress(), BURN_ROLE) } beforeEach(async () => { - await network.provider.request({ - method: 'hardhat_reset', - params: [ - { - forking: { - jsonRpcUrl: config.networks.hardhat.forking.url - } - } - ] - }) + const rpc = config.networks.hardhat.forking.url + const blockToForkFrom = config.networks.hardhat.forking.blockNumber + await hardhatReset(network.provider, rpc, blockToForkFrom) const RewardsManager = await ethers.getContractFactory('RewardsManager') const EpochsManager = await ethers.getContractFactory('EpochsManager') @@ -67,12 +71,12 @@ describe('RewardsManager', () => { await Promise.all([...pntHolders, randomGuy].map((_dest) => sendEthers(owner, _dest, '1'))) acl = await ethers.getContractAt(AclAbi, ACL_ADDRESS) + tokenManager = await ethers.getContractAt(TokenManagerAbi, TOKEN_MANAGER_ADDRESS) pnt = await TestToken.deploy('PNT', 'PNT') - await Promise.all(pntHolders.map((_holder) => sendPnt(owner, _holder, '400000'))) + await Promise.all(pntHolders.map((_holder) => sendPnt(owner, _holder.address, '400000'))) dandelionVoting = await MockDandelionVotingContract.deploy() - await dandelionVoting.setTestStartDate(EPOCH_DURATION * 1000) // this is needed to don't break normal tests epochsManager = await upgrades.deployProxy(EpochsManager, [EPOCH_DURATION, 0], { initializer: 'initialize', @@ -81,7 +85,13 @@ describe('RewardsManager', () => { rewardsManager = await upgrades.deployProxy( RewardsManager, - [epochsManager.address, await dandelionVoting.getAddress(), await pnt.getAddress(), TOKEN_MANAGER_ADDRESS, 100], + [ + await epochsManager.getAddress(), + await dandelionVoting.getAddress(), + await pnt.getAddress(), + await tokenManager.getAddress(), + PNT_MAX_TOTAL_SUPPLY + ], { initializer: 'initialize', kind: 'uups' @@ -93,11 +103,11 @@ describe('RewardsManager', () => { it('should deploy correctly', async () => { expect(await rewardsManager.token()).to.eq(await pnt.getAddress()) - expect(await rewardsManager.tokenManager()).to.eq(TOKEN_MANAGER_ADDRESS) + expect(await rewardsManager.tokenManager()).to.eq(await tokenManager.getAddress()) }) it('should be possible to deposit tokens', async () => { - const amount = 100 + const amount = 100n expect(await epochsManager.currentEpoch()).to.be.eq(0) await rewardsManager.grantRole(DEPOSIT_REWARD_ROLE, owner.address) await pnt.approve(await rewardsManager.getAddress(), amount) @@ -106,13 +116,18 @@ describe('RewardsManager', () => { await rewardsManager.depositForEpoch(0, amount) const pntOwnerBalancePost = await pnt.balanceOf(owner.address) const pntRewardsManagerBalancePost = await pnt.balanceOf(await rewardsManager.getAddress()) - expect(pntOwnerBalancePost).to.be.eq(pntOwnerBalancePre.sub(amount)) - expect(pntRewardsManagerBalancePost).to.be.eq(pntRewardsManagerBalancePre.add(amount)) + expect(pntOwnerBalancePost).to.be.eq(pntOwnerBalancePre - amount) + expect(pntRewardsManagerBalancePost).to.be.eq(pntRewardsManagerBalancePre + amount) expect(await rewardsManager.depositedAmountByEpoch(0)).to.be.eq(amount) }) it('should not be able to register for rewards without voting', async () => { - const amount = 100 + const amount = (ethers.parseUnits('660000') * 10n) / 100n + await setPermission(owner.address, await tokenManager.getAddress(), MINT_ROLE) + await tokenManager.mint(pntHolder1.address, ethers.parseUnits('200000')) + await tokenManager.mint(pntHolder2.address, ethers.parseUnits('400000')) + await tokenManager.mint(pntHolder3.address, ethers.parseUnits('50000')) + await tokenManager.mint(pntHolder4.address, ethers.parseUnits('10000')) expect(await epochsManager.currentEpoch()).to.be.eq(0) await rewardsManager.grantRole(DEPOSIT_REWARD_ROLE, owner.address) await pnt.approve(await rewardsManager.getAddress(), amount) @@ -121,10 +136,10 @@ describe('RewardsManager', () => { await rewardsManager.depositForEpoch(0, amount) const pntOwnerBalancePost = await pnt.balanceOf(owner.address) const pntRewardsManagerBalancePost = await pnt.balanceOf(await rewardsManager.getAddress()) - expect(pntOwnerBalancePost).to.be.eq(pntOwnerBalancePre.sub(amount)) - expect(pntRewardsManagerBalancePost).to.be.eq(pntRewardsManagerBalancePre.add(amount)) + expect(pntOwnerBalancePost).to.be.eq(pntOwnerBalancePre - amount) + expect(pntRewardsManagerBalancePost).to.be.eq(pntRewardsManagerBalancePre + amount) await time.increase(ONE_DAY) - await dandelionVoting.setTestStartDate(await time.latest()) + await dandelionVoting.setTestStartDate() await Promise.all( R.zip(pntHolders, [1, 1, 2, 0]).map(([holder, status]) => dandelionVoting.setTestVoteState(holder.address, status) @@ -132,14 +147,16 @@ describe('RewardsManager', () => { ) await time.increase(ONE_MONTH + ONE_DAY) await expect( - rewardsManager.connect(randomGuy).registerRewards(0, [pntHolder1.address, pntHolder2.address, pntHolder3.address]) + rewardsManager + .connect(randomGuy) + .registerRewardsForEpoch(0, [pntHolder1.address, pntHolder2.address, pntHolder3.address]) ).to.not.be.reverted - await expect(rewardsManager.connect(randomGuy).registerRewards(0, [pntHolder3.address, pntHolder4.address])).to.not - .be.reverted - expect(await rewardsManager.lockedRewardByEpoch(0, pntHolder1.address)).to.be.eq(1) - expect(await rewardsManager.lockedRewardByEpoch(0, pntHolder2.address)).to.be.eq(1) - expect(await rewardsManager.lockedRewardByEpoch(0, pntHolder3.address)).to.be.eq(1) - expect(await rewardsManager.lockedRewardByEpoch(0, pntHolder4.address)).to.be.eq(0) + await expect(rewardsManager.connect(randomGuy).registerRewardsForEpoch(0, [pntHolder3.address, pntHolder4.address])) + .to.not.be.reverted + expect(await rewardsManager.lockedRewardByEpoch(0, pntHolder1.address)).to.be.eq(ethers.parseUnits('20000')) + expect(await rewardsManager.lockedRewardByEpoch(0, pntHolder2.address)).to.be.eq(ethers.parseUnits('40000')) + expect(await rewardsManager.lockedRewardByEpoch(0, pntHolder3.address)).to.be.eq(ethers.parseUnits('5000')) + expect(await rewardsManager.lockedRewardByEpoch(0, pntHolder4.address)).to.be.eq(ethers.parseUnits('0')) await expect(rewardsManager.connect(pntHolder1).claimRewardByEpoch(0)).to.be.revertedWithCustomError( rewardsManager, 'TooEarly' @@ -147,11 +164,15 @@ describe('RewardsManager', () => { await time.increase(ONE_MONTH * 12 + ONE_DAY) await expect(rewardsManager.connect(pntHolder1).claimRewardByEpoch(0)) .to.emit(pnt, 'Transfer') - .withArgs(await rewardsManager.getAddress(), pntHolder1.address, 1) + .withArgs(await rewardsManager.getAddress(), pntHolder1.address, ethers.parseUnits('20000')) + await expect(rewardsManager.connect(pntHolder1).claimRewardByEpoch(0)).to.be.revertedWithCustomError( + rewardsManager, + 'NothingToClaim' + ) await time.increase(ONE_MONTH) await expect(rewardsManager.connect(pntHolder2).claimRewardByEpoch(0)) .to.emit(pnt, 'Transfer') - .withArgs(await rewardsManager.getAddress(), pntHolder2.address, 1) + .withArgs(await rewardsManager.getAddress(), pntHolder2.address, ethers.parseUnits('40000')) await expect(rewardsManager.connect(randomGuy).claimRewardByEpoch(0)).to.be.revertedWithCustomError( rewardsManager, 'NothingToClaim' diff --git a/test/abi/TokenManager.json b/test/abi/TokenManager.json new file mode 100644 index 0000000..af9f790 --- /dev/null +++ b/test/abi/TokenManager.json @@ -0,0 +1,755 @@ +[ + { + "constant": true, + "inputs": [], + "name": "hasInitialized", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "MAX_VESTINGS_PER_ADDRESS", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_holder", + "type": "address" + } + ], + "name": "spendableBalanceOf", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_receiver", + "type": "address" + }, + { + "name": "_amount", + "type": "uint256" + }, + { + "name": "_start", + "type": "uint64" + }, + { + "name": "_cliff", + "type": "uint64" + }, + { + "name": "_vested", + "type": "uint64" + }, + { + "name": "_revokable", + "type": "bool" + } + ], + "name": "assignVested", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_script", + "type": "bytes" + } + ], + "name": "getEVMScriptExecutor", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getRecoveryVault", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_recipient", + "type": "address" + }, + { + "name": "_vestingId", + "type": "uint256" + } + ], + "name": "getVesting", + "outputs": [ + { + "name": "amount", + "type": "uint256" + }, + { + "name": "start", + "type": "uint64" + }, + { + "name": "cliff", + "type": "uint64" + }, + { + "name": "vesting", + "type": "uint64" + }, + { + "name": "revokable", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_receiver", + "type": "address" + }, + { + "name": "_amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_amount", + "type": "uint256" + } + ], + "name": "onTransfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_holder", + "type": "address" + }, + { + "name": "_time", + "type": "uint256" + } + ], + "name": "transferableBalance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_token", + "type": "address" + } + ], + "name": "allowRecoverability", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "appId", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "ISSUE_ROLE", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getInitializationBlock", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "vestingsLengths", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_token", + "type": "address" + } + ], + "name": "transferToVault", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_holder", + "type": "address" + }, + { + "name": "_amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_sender", + "type": "address" + }, + { + "name": "_role", + "type": "bytes32" + }, + { + "name": "_params", + "type": "uint256[]" + } + ], + "name": "canPerform", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getEVMScriptRegistry", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "ASSIGN_ROLE", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "BURN_ROLE", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_receiver", + "type": "address" + }, + { + "name": "_amount", + "type": "uint256" + } + ], + "name": "assign", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_sender", + "type": "address" + }, + { + "name": "", + "type": "bytes" + } + ], + "name": "canForward", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_amount", + "type": "uint256" + } + ], + "name": "issue", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "kernel", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_evmScript", + "type": "bytes" + } + ], + "name": "forward", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "", + "type": "address" + }, + { + "name": "", + "type": "address" + }, + { + "name": "", + "type": "uint256" + } + ], + "name": "onApprove", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "isPetrified", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_token", + "type": "address" + }, + { + "name": "_transferable", + "type": "bool" + }, + { + "name": "_maxAccountTokens", + "type": "uint256" + } + ], + "name": "initialize", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "MINT_ROLE", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "maxAccountTokens", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "REVOKE_VESTINGS_ROLE", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "proxyPayment", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_holder", + "type": "address" + }, + { + "name": "_vestingId", + "type": "uint256" + } + ], + "name": "revokeVesting", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "token", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "isForwarder", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "name": "vestingId", + "type": "uint256" + }, + { + "indexed": false, + "name": "amount", + "type": "uint256" + } + ], + "name": "NewVesting", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "name": "vestingId", + "type": "uint256" + }, + { + "indexed": false, + "name": "nonVestedAmount", + "type": "uint256" + } + ], + "name": "RevokeVesting", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "executor", + "type": "address" + }, + { + "indexed": false, + "name": "script", + "type": "bytes" + }, + { + "indexed": false, + "name": "input", + "type": "bytes" + }, + { + "indexed": false, + "name": "returnData", + "type": "bytes" + } + ], + "name": "ScriptResult", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "vault", + "type": "address" + }, + { + "indexed": true, + "name": "token", + "type": "address" + }, + { + "indexed": false, + "name": "amount", + "type": "uint256" + } + ], + "name": "RecoverToVault", + "type": "event" + } +] \ No newline at end of file From c7d2e757c776175c1cff14c1460de4df7791d3c3 Mon Sep 17 00:00:00 2001 From: Alain Olivier Date: Fri, 26 Jan 2024 22:04:24 +0100 Subject: [PATCH 04/15] refactor(contracts): optimize RewardsManager --- contracts/core/RewardsManager.sol | 82 ++++++++++++++----------------- 1 file changed, 37 insertions(+), 45 deletions(-) diff --git a/contracts/core/RewardsManager.sol b/contracts/core/RewardsManager.sol index 5e2d2e2..ad9abce 100644 --- a/contracts/core/RewardsManager.sol +++ b/contracts/core/RewardsManager.sol @@ -73,11 +73,11 @@ contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, Acce function registerRewardsForEpoch(uint16 epoch, address[] calldata stakers) external { uint16 currentEpoch = IEpochsManager(epochsManager).currentEpoch(); if (epoch >= currentEpoch) revert Errors.InvalidEpoch(); + (bool[] memory hasVoted, uint256[] memory amounts) = _getVotesAndBalancesForEpoch(epoch, stakers); for (uint256 i = 0; i < stakers.length; i++) { if (lockedRewardByEpoch[epoch][stakers[i]] > 0) continue; - bool hasVoted = _hasVotedInEpoch(epoch, stakers[i]); - uint256 amount = _calculateStakerRewardForEpoch(epoch, stakers[i]); - if (hasVoted && amount > 0) { + uint256 amount = amounts[i]; + if (hasVoted[i] && amount > 0) { ITokenManager(tokenManager).mint(stakers[i], amount); _checkTotalSupply(); lockedRewardByEpoch[epoch][stakers[i]] = amount; @@ -98,14 +98,6 @@ contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, Acce function _authorizeUpgrade(address) internal override onlyRole(Roles.UPGRADE_ROLE) {} - function _calculateStakerRewardForEpoch(uint16 epoch, address staker) private returns (uint256) { - address minime = ITokenManager(tokenManager).token(); - (, , , uint64 snapshotBlock) = _getLastVoteInEpoch(epoch); - uint256 supply = IMinimeToken(minime).totalSupplyAt(snapshotBlock); - uint256 balance = IMinimeToken(minime).balanceOfAt(staker, snapshotBlock); - return (depositedAmountByEpoch[epoch] * balance) / supply; - } - function _checkTotalSupply() internal { address minime = ITokenManager(tokenManager).token(); if (IERC20Upgradeable(minime).totalSupply() > maxTotalSupply) { @@ -121,48 +113,48 @@ contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, Acce return (epochStartDate, epochEndDate); } - function _getLastVoteInEpoch(uint16 epoch) private returns (uint256, uint64, uint64, uint64) { - uint256 numberOfVotes = IDandelionVoting(dandelionVoting).votesLength(); - uint64 voteDuration = IDandelionVoting(dandelionVoting).duration(); - (uint256 epochStartDate, uint256 epochEndDate) = _getEpochTimestamps(epoch); - for (uint256 voteId = numberOfVotes; voteId >= 1; ) { - (, , uint64 startDate, uint64 executionDate, uint64 snapshotBlock, , , , , , ) = IDandelionVoting( - dandelionVoting - ).getVote(voteId); - uint64 voteEndDate = startDate + voteDuration; - if (voteEndDate >= epochStartDate && voteEndDate <= epochEndDate) { - return (voteId, startDate, executionDate, snapshotBlock); - } - unchecked { - --voteId; - } - } - revert Errors.NoVoteInEpoch(); - } + function _getVotesAndBalancesForEpoch( + uint16 epoch, + address[] memory stakers + ) private returns (bool[] memory, uint256[] memory) { + IDandelionVoting votingContract = IDandelionVoting(dandelionVoting); + IMinimeToken minime = IMinimeToken(ITokenManager(tokenManager).token()); - function _hasVotedInEpoch(uint16 epoch, address staker) private returns (bool) { - uint256 numberOfVotes = IDandelionVoting(dandelionVoting).votesLength(); - uint64 voteDuration = IDandelionVoting(dandelionVoting).duration(); + uint256 numberOfVotes = votingContract.votesLength(); + uint64 voteDuration = votingContract.duration(); (uint256 epochStartDate, uint256 epochEndDate) = _getEpochTimestamps(epoch); - for (uint256 voteId = numberOfVotes; voteId >= 1; ) { - (, , uint64 voteStartDate, , , , , , , , ) = IDandelionVoting(dandelionVoting).getVote(voteId); - uint64 voteEndDate = voteStartDate + voteDuration; + uint64 lastVoteSnapshotBlock; + uint256 supply; + + bool[] memory hasVoted = new bool[](stakers.length); + uint256[] memory amounts = new uint256[](stakers.length); + + for (uint256 voteId = numberOfVotes; voteId >= 1; voteId--) { + (, , uint64 startDate, , uint64 snapshotBlock, , , , , , ) = votingContract.getVote(voteId); + uint64 voteEndDate = startDate + voteDuration; if (voteEndDate >= epochStartDate && voteEndDate <= epochEndDate) { - if ( - IDandelionVoting(dandelionVoting).getVoterState(voteId, staker) != - IDandelionVoting.VoterState.Absent - ) { - unchecked { - return true; + if (lastVoteSnapshotBlock == 0) { + lastVoteSnapshotBlock = snapshotBlock; + supply = minime.totalSupplyAt(lastVoteSnapshotBlock); + } + for (uint256 i = 0; i < stakers.length; i++) { + if ( + !hasVoted[i] && + votingContract.getVoterState(voteId, stakers[i]) != IDandelionVoting.VoterState.Absent + ) { + hasVoted[i] = true; + uint256 balance = minime.balanceOfAt(stakers[i], lastVoteSnapshotBlock); + amounts[i] = (depositedAmountByEpoch[epoch] * balance) / supply; } } } - unchecked { - --voteId; - } } - return false; + if (lastVoteSnapshotBlock == 0) { + revert Errors.NoVoteInEpoch(); + } + + return (hasVoted, amounts); } } From 2d3b881bac7585b200859f29fe2229175e7a9481 Mon Sep 17 00:00:00 2001 From: Alain Olivier Date: Wed, 14 Feb 2024 11:16:43 +0100 Subject: [PATCH 05/15] fix(lending-manager): revert in claimRewardByEpoch() in case of no votes --- contracts/core/LendingManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/core/LendingManager.sol b/contracts/core/LendingManager.sol index a3aa52f..835e309 100644 --- a/contracts/core/LendingManager.sol +++ b/contracts/core/LendingManager.sol @@ -139,7 +139,7 @@ contract LendingManager is ILendingManager, Initializable, UUPSUpgradeable, Forw } (uint256 numberOfVotes, uint256 votedVotes) = getLenderVotingStateByEpoch(lender, epoch); - if (numberOfVotes > votedVotes) revert Errors.NotPartecipatedInGovernanceAtEpoch(epoch); + if (numberOfVotes == 0 || numberOfVotes > votedVotes) revert Errors.NotPartecipatedInGovernanceAtEpoch(epoch); uint256 reward = claimableRewardsByEpochOf(lender, asset, epoch); if (reward == 0) { From 2698e635efac365503d2470dd2190c16fc8d8661 Mon Sep 17 00:00:00 2001 From: Alain Olivier Date: Fri, 26 Jan 2024 22:47:34 +0100 Subject: [PATCH 06/15] test: refactor dandelion voting mock --- contracts/test/MockDandelionVoting.sol | 37 +++++++------ test/FeesManager.test.js | 14 ++++- test/LendingManager.test.js | 76 +++++++++++++++++++------- test/RewardsManager.test.js | 25 +++++++-- test/constants.js | 7 ++- 5 files changed, 116 insertions(+), 43 deletions(-) diff --git a/contracts/test/MockDandelionVoting.sol b/contracts/test/MockDandelionVoting.sol index 12ba5d3..a7cfbe2 100644 --- a/contracts/test/MockDandelionVoting.sol +++ b/contracts/test/MockDandelionVoting.sol @@ -2,21 +2,26 @@ pragma solidity ^0.8.17; contract MockDandelionVotingContract { - uint64 private _testStartDate; - uint64 private _testSnapshotBlock; - mapping(address => uint256) private _testVoteState; + struct Vote { + mapping(address => uint256) votersState; + uint64 startDate; + uint64 snapshotBlock; + } + mapping(uint256 => Vote) private _votes; + uint256 id; - function setTestVoteState(address voter, uint256 testVoteState_) external { - _testVoteState[voter] = testVoteState_; + function setTestVoteState(uint256 voteId, address voter, uint256 state) external { + _votes[voteId].votersState[voter] = state; } - function setTestStartDate() external { - _testStartDate = uint64(block.timestamp); - _testSnapshotBlock = uint64(block.number); + function newVote() external { + uint256 newId = ++id; + _votes[newId].startDate = uint64(block.timestamp); + _votes[newId].snapshotBlock = uint64(block.number); } - function votesLength() external pure returns (uint256) { - return 1; + function votesLength() external view returns (uint256) { + return id; } function duration() public pure returns (uint64) { @@ -25,7 +30,7 @@ contract MockDandelionVotingContract { // 0 values are just for testing since here we need to test if a lender voted to one or more votes within an epoch function getVote( - uint256 + uint256 voteId ) external view returns ( @@ -44,9 +49,9 @@ contract MockDandelionVotingContract { { open = false; executed = true; - startDate = _testStartDate; - executionDate = _testStartDate + duration(); - snapshotBlock = _testSnapshotBlock; + startDate = _votes[voteId].startDate; + executionDate = _votes[voteId].startDate + duration(); + snapshotBlock = _votes[voteId].snapshotBlock; votingPower = 0; supportRequired = 0; minAcceptQuorum = 0; @@ -55,7 +60,7 @@ contract MockDandelionVotingContract { script = ""; } - function getVoterState(uint256, address voter) external view returns (uint256) { - return _testVoteState[voter]; + function getVoterState(uint256 voteId, address voter) external view returns (uint256) { + return _votes[voteId].votersState[voter]; } } diff --git a/test/FeesManager.test.js b/test/FeesManager.test.js index 6ed9416..ae2e81f 100644 --- a/test/FeesManager.test.js +++ b/test/FeesManager.test.js @@ -11,7 +11,8 @@ const { PNT_HOLDER_1_ADDRESS, PNT_HOLDER_2_ADDRESS, PNT_MAX_TOTAL_SUPPLY, - TOKEN_MANAGER_ADDRESS + TOKEN_MANAGER_ADDRESS, + VOTE_STATUS } = require('./constants') const { BORROW_ROLE, @@ -91,7 +92,6 @@ describe('FeesManager', () => { pnt = await TestToken.deploy('PNT', 'PNT') acl = ACL.attach(ACL_ADDRESS) dandelionVoting = await MockDandelionVotingContract.deploy() - await dandelionVoting.setTestStartDate(EPOCH_DURATION * 1000) // this is needed to don't break normal tests await pnt.connect(owner).transfer(pntHolder1.address, ethers.parseEther('1000000')) await pnt.connect(owner).transfer(pntHolder2.address, ethers.parseEther('1000000')) @@ -241,6 +241,8 @@ describe('FeesManager', () => { const fee = ethers.parseEther('100') await pnt.connect(pntHolder1).approve(await feesManager.getAddress(), fee) await feesManager.connect(pntHolder1).depositFee(await pnt.getAddress(), fee) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder1.address, VOTE_STATUS.YES) await time.increase(EPOCH_DURATION) expect(await epochsManager.currentEpoch()).to.be.equal(2) @@ -293,6 +295,8 @@ describe('FeesManager', () => { const fee = ethers.parseEther('100') await pnt.connect(pntHolder1).approve(await feesManager.getAddress(), fee) await feesManager.connect(pntHolder1).depositFee(await pnt.getAddress(), fee) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder1.address, VOTE_STATUS.YES) await time.increase(EPOCH_DURATION) expect(await epochsManager.currentEpoch()).to.be.equal(2) @@ -357,6 +361,8 @@ describe('FeesManager', () => { const fee = ethers.parseEther('100') await pnt.connect(pntHolder1).approve(await feesManager.getAddress(), fee) await feesManager.connect(pntHolder1).depositFee(await pnt.getAddress(), fee) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder1.address, VOTE_STATUS.YES) await time.increase(EPOCH_DURATION) expect(await epochsManager.currentEpoch()).to.be.equal(2) @@ -446,6 +452,8 @@ describe('FeesManager', () => { const fee = ethers.parseEther('100') await pnt.connect(pntHolder1).approve(await feesManager.getAddress(), fee) await feesManager.connect(pntHolder1).depositFee(await pnt.getAddress(), fee) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder1.address, VOTE_STATUS.YES) await time.increase(EPOCH_DURATION) expect(await epochsManager.currentEpoch()).to.be.equal(2) @@ -776,6 +784,8 @@ describe('FeesManager', () => { const fee = ethers.parseEther('100') await pnt.connect(pntHolder1).approve(await feesManager.getAddress(), fee) await feesManager.connect(pntHolder1).depositFee(await pnt.getAddress(), fee) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder1.address, VOTE_STATUS.YES) await time.increase(EPOCH_DURATION) expect(await epochsManager.currentEpoch()).to.be.equal(2) diff --git a/test/LendingManager.test.js b/test/LendingManager.test.js index 8f6939e..10b4735 100644 --- a/test/LendingManager.test.js +++ b/test/LendingManager.test.js @@ -13,7 +13,8 @@ const { PNT_HOLDER_1_ADDRESS, PNT_HOLDER_2_ADDRESS, PNT_MAX_TOTAL_SUPPLY, - TOKEN_MANAGER_ADDRESS + TOKEN_MANAGER_ADDRESS, + VOTE_STATUS } = require('./constants') const { BORROW_ROLE, @@ -67,7 +68,6 @@ describe('LendingManager', () => { pnt = await TestToken.deploy('PNT', 'PNT') acl = ACL.attach(ACL_ADDRESS) dandelionVoting = await MockDandelionVotingContract.deploy() - await dandelionVoting.setTestStartDate(EPOCH_DURATION * 1000) // this is needed to don't break normal tests await pnt.connect(owner).transfer(pntHolder1.address, ethers.parseEther('400000')) await pnt.connect(owner).transfer(pntHolder2.address, ethers.parseEther('400000')) @@ -1095,13 +1095,23 @@ describe('LendingManager', () => { expect(await lendingManager.totalWeightByEpoch(5)).to.be.eq(5000) await lendingManager.depositReward(await pnt.getAddress(), 1, depositRewardAmount) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder1.address, VOTE_STATUS.YES) + await time.increase(EPOCH_DURATION) await expect(lendingManager.connect(pntHolder1).claimRewardByEpoch(await pnt.getAddress(), 1)) .to.emit(lendingManager, 'RewardClaimed') .withArgs(pntHolder1.address, await pnt.getAddress(), 1, depositRewardAmount) await lendingManager.depositReward(await pnt.getAddress(), 2, depositRewardAmount) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder1.address, VOTE_STATUS.YES) + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder2.address, VOTE_STATUS.YES) + await time.increase(EPOCH_DURATION) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder1.address, VOTE_STATUS.YES) + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder2.address, VOTE_STATUS.YES) await expect(lendingManager.connect(pntHolder1).claimRewardByEpoch(await pnt.getAddress(), 2)) .to.emit(lendingManager, 'RewardClaimed') .withArgs(pntHolder1.address, await pnt.getAddress(), 2, ethers.parseEther('8823.529411764705882352')) @@ -1110,6 +1120,9 @@ describe('LendingManager', () => { .withArgs(pntHolder2.address, await pnt.getAddress(), 2, ethers.parseEther('1176.470588235294117647')) await time.increase(EPOCH_DURATION) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder1.address, VOTE_STATUS.YES) + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder2.address, VOTE_STATUS.YES) await expect( lendingManager.connect(pntHolder1).claimRewardByEpoch(await pnt.getAddress(), 3) ).to.be.revertedWithCustomError(lendingManager, 'NothingToClaim') @@ -1126,6 +1139,10 @@ describe('LendingManager', () => { ).to.be.revertedWithCustomError(lendingManager, 'NothingToClaim') await lendingManager.depositReward(await pnt.getAddress(), 5, depositRewardAmount) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder1.address, VOTE_STATUS.YES) + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder2.address, VOTE_STATUS.YES) + await time.increase(EPOCH_DURATION) // ((5k / 5k) + (1/1)) / 2 = 1 ---> 10000 * 1 = 10000 await expect(lendingManager.connect(pntHolder2).claimRewardByEpoch(await pnt.getAddress(), 5)) @@ -1184,7 +1201,13 @@ describe('LendingManager', () => { expect(await lendingManager.totalWeightByEpoch(2)).to.be.eq(5000) await lendingManager.depositReward(await pnt.getAddress(), 1, depositRewardAmount) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder1.address, VOTE_STATUS.YES) + await time.increase(EPOCH_DURATION) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder2.address, VOTE_STATUS.YES) + await expect(lendingManager.connect(pntHolder1).claimRewardByEpoch(await pnt.getAddress(), 1)) .to.emit(lendingManager, 'RewardClaimed') .withArgs(pntHolder1.address, await pnt.getAddress(), 1, depositRewardAmount) @@ -1237,9 +1260,14 @@ describe('LendingManager', () => { await time.increase(EPOCH_DURATION) expect(await epochsManager.currentEpoch()).to.be.equal(1) await lendingManager.connect(pntHolder2).lend(pntHolder2.address, depositAmountPntHolder2, duration) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder1.address, VOTE_STATUS.YES) await time.increase(EPOCH_DURATION) expect(await epochsManager.currentEpoch()).to.be.equal(2) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder1.address, VOTE_STATUS.YES) + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder2.address, VOTE_STATUS.NO) await lendingManager.depositReward(await pnt.getAddress(), 1, depositRewardAmount) await expect(lendingManager.connect(pntHolder1).claimRewardByEpoch(await pnt.getAddress(), 1)) @@ -1248,6 +1276,8 @@ describe('LendingManager', () => { await time.increase(EPOCH_DURATION) expect(await epochsManager.currentEpoch()).to.be.equal(3) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder1.address, VOTE_STATUS.YES) await lendingManager.depositReward(await pnt.getAddress(), 2, depositRewardAmount) // (50k * 3) / (50k*3 + 5k*4) = 0.8823529411764705882352 ---> 1000 * 0.8823529411764705882352 = 8823.529411764705882352 @@ -1261,6 +1291,8 @@ describe('LendingManager', () => { await time.increase(EPOCH_DURATION) expect(await epochsManager.currentEpoch()).to.be.equal(4) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder1.address, VOTE_STATUS.YES) await expect( lendingManager.connect(pntHolder1).claimRewardByEpoch(await pnt.getAddress(), 3) @@ -1272,6 +1304,9 @@ describe('LendingManager', () => { await time.increase(EPOCH_DURATION) expect(await epochsManager.currentEpoch()).to.be.equal(5) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(await dandelionVoting.votesLength(), pntHolder2.address, VOTE_STATUS.YES) + await expect( lendingManager.connect(pntHolder1).claimRewardByEpoch(await pnt.getAddress(), 4) ).to.be.revertedWithCustomError(lendingManager, 'NothingToClaim') @@ -1787,7 +1822,6 @@ describe('LendingManager', () => { const depositRewardAmount = ethers.parseEther('10000') const depositAmountPntHolder1 = ethers.parseEther('50000') const depositAmountPntHolder2 = ethers.parseEther('5000') - const startFirstEpochTimestamp = await epochsManager.startFirstEpochTimestamp() const duration = EPOCH_DURATION * 5 await pnt.connect(pntHolder1).approve(await lendingManager.getAddress(), INFINITE) @@ -1800,32 +1834,36 @@ describe('LendingManager', () => { expect(await epochsManager.currentEpoch()).to.be.equal(1) await lendingManager.connect(pntHolder2).lend(pntHolder2.address, depositAmountPntHolder2, duration) - // making the vote available at epoch 1 - await dandelionVoting.setTestStartDate(startFirstEpochTimestamp + BigInt(EPOCH_DURATION + ONE_DAY)) - await dandelionVoting.setTestVoteState(0) + // create a vote + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(1, pntHolder1.address, VOTE_STATUS.ABSENT) await lendingManager.depositReward(await pnt.getAddress(), 1, depositRewardAmount) await time.increase(EPOCH_DURATION) - await expect( - lendingManager.connect(pntHolder1).claimRewardByEpoch(await pnt.getAddress(), 1) - ).to.be.revertedWithCustomError(lendingManager, 'NotPartecipatedInGovernanceAtEpoch') + await expect(lendingManager.connect(pntHolder1).claimRewardByEpoch(await pnt.getAddress(), 1)) + .to.be.revertedWithCustomError(lendingManager, 'NotPartecipatedInGovernanceAtEpoch') + .withArgs(1) await lendingManager.depositReward(await pnt.getAddress(), 2, depositRewardAmount) await time.increase(EPOCH_DURATION) - - // making the vote available at epoch 2 - await dandelionVoting.setTestStartDate(startFirstEpochTimestamp + BigInt(EPOCH_DURATION * 2 + ONE_DAY)) - await dandelionVoting.setTestVoteState(1) + // no votes at epoch 2 + await dandelionVoting.setTestVoteState(1, pntHolder1.address, VOTE_STATUS.YES) + await dandelionVoting.setTestVoteState(1, pntHolder2.address, VOTE_STATUS.YES) await expect(lendingManager.connect(pntHolder1).claimRewardByEpoch(await pnt.getAddress(), 2)) - .to.emit(lendingManager, 'RewardClaimed') - .withArgs(pntHolder1.address, await pnt.getAddress(), 2, ethers.parseEther('8823.529411764705882352')) - await expect(lendingManager.connect(pntHolder2).claimRewardByEpoch(await pnt.getAddress(), 2)) - .to.emit(lendingManager, 'RewardClaimed') - .withArgs(pntHolder2.address, await pnt.getAddress(), 2, ethers.parseEther('1176.470588235294117647')) + .to.be.revertedWithCustomError(lendingManager, 'NotPartecipatedInGovernanceAtEpoch') + .withArgs(2) + await expect(lendingManager.connect(pntHolder1).claimRewardByEpoch(await pnt.getAddress(), 2)) + .to.be.revertedWithCustomError(lendingManager, 'NotPartecipatedInGovernanceAtEpoch') + .withArgs(2) + + await time.increase(EPOCH_DURATION * 2) + await dandelionVoting.newVote() + await dandelionVoting.setTestVoteState(2, pntHolder1.address, VOTE_STATUS.YES) + await dandelionVoting.setTestVoteState(2, pntHolder2.address, VOTE_STATUS.YES) await lendingManager.depositReward(await pnt.getAddress(), 5, depositRewardAmount) - await time.increase(EPOCH_DURATION * 3) + await time.increase(EPOCH_DURATION) // ((5k / 5k) + (1/1)) / 2 = 1 ---> 10000 * 1 = 10000 await expect(lendingManager.connect(pntHolder2).claimRewardByEpoch(await pnt.getAddress(), 5)) .to.emit(lendingManager, 'RewardClaimed') diff --git a/test/RewardsManager.test.js b/test/RewardsManager.test.js index 82b6c3b..f0e52d2 100644 --- a/test/RewardsManager.test.js +++ b/test/RewardsManager.test.js @@ -12,7 +12,8 @@ const { ONE_MONTH, DAO_CREATOR, ACL_ADDRESS, - PNT_MAX_TOTAL_SUPPLY + PNT_MAX_TOTAL_SUPPLY, + VOTE_STATUS } = require('./constants') const { DEPOSIT_REWARD_ROLE, MINT_ROLE, BURN_ROLE } = require('./roles') const { hardhatReset } = require('./utils/hardhat-reset') @@ -68,7 +69,7 @@ describe('RewardsManager', () => { pntHolder4 = ethers.Wallet.createRandom().connect(ethers.provider) pntHolders = [pntHolder1, pntHolder2, pntHolder3, pntHolder4] - await Promise.all([...pntHolders, randomGuy].map((_dest) => sendEthers(owner, _dest, '1'))) + await Promise.all([...pntHolders, daoCreator, randomGuy].map((_dest) => sendEthers(owner, _dest, '1001'))) acl = await ethers.getContractAt(AclAbi, ACL_ADDRESS) tokenManager = await ethers.getContractAt(TokenManagerAbi, TOKEN_MANAGER_ADDRESS) @@ -139,10 +140,24 @@ describe('RewardsManager', () => { expect(pntOwnerBalancePost).to.be.eq(pntOwnerBalancePre - amount) expect(pntRewardsManagerBalancePost).to.be.eq(pntRewardsManagerBalancePre + amount) await time.increase(ONE_DAY) - await dandelionVoting.setTestStartDate() + await dandelionVoting.newVote() await Promise.all( - R.zip(pntHolders, [1, 1, 2, 0]).map(([holder, status]) => - dandelionVoting.setTestVoteState(holder.address, status) + R.zip(pntHolders, [VOTE_STATUS.YES, VOTE_STATUS.YES, VOTE_STATUS.ABSENT, VOTE_STATUS.ABSENT]).map( + ([holder, status]) => dandelionVoting.setTestVoteState(1, holder.address, status) + ) + ) + await time.increase(ONE_DAY * 4) + await dandelionVoting.newVote() + await Promise.all( + R.zip(pntHolders, [VOTE_STATUS.YES, VOTE_STATUS.ABSENT, VOTE_STATUS.YES, VOTE_STATUS.ABSENT]).map( + ([holder, status]) => dandelionVoting.setTestVoteState(2, holder.address, status) + ) + ) + await time.increase(ONE_DAY * 4) + await dandelionVoting.newVote() + await Promise.all( + R.zip(pntHolders, [VOTE_STATUS.ABSENT, VOTE_STATUS.ABSENT, VOTE_STATUS.YES, VOTE_STATUS.ABSENT]).map( + ([holder, status]) => dandelionVoting.setTestVoteState(3, holder.address, status) ) ) await time.increase(ONE_MONTH + ONE_DAY) diff --git a/test/constants.js b/test/constants.js index db6b1a3..7fe9115 100644 --- a/test/constants.js +++ b/test/constants.js @@ -36,5 +36,10 @@ module.exports = { DAO_CREATOR: '0x08544a580EDC2C3F27689b740F8257A29166FC77', ACL_CONTRACT: '0x50b2b8e429cB51bD43cD3E690e5BEB9eb674f6d7', ZERO_ADDRESS: '0x0000000000000000000000000000000000000000', - ONE_HOUR_IN_S: 3600 + ONE_HOUR_IN_S: 3600, + VOTE_STATUS: { + ABSENT: 0, + YES: 1, + NO: 2 + } } From 4bac8985f4769ee19d0f85250ba08d6da7760c8d Mon Sep 17 00:00:00 2001 From: Alain Olivier Date: Wed, 14 Feb 2024 13:25:16 +0100 Subject: [PATCH 07/15] test(rewards-manager): refactor tests --- test/RewardsManager.test.js | 116 +++++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 40 deletions(-) diff --git a/test/RewardsManager.test.js b/test/RewardsManager.test.js index f0e52d2..269d7e9 100644 --- a/test/RewardsManager.test.js +++ b/test/RewardsManager.test.js @@ -32,6 +32,7 @@ describe('RewardsManager', () => { rewardsManager, acl, tokenManager, + daoPnt, daoCreator const setPermission = async (entity, app, role) => acl.connect(daoCreator).grantPermission(entity, app, role) @@ -49,6 +50,42 @@ describe('RewardsManager', () => { await setPermission(await rewardsManager.getAddress(), await tokenManager.getAddress(), BURN_ROLE) } + const depositRewardsForEpoch = async (_amount, _epoch) => { + await rewardsManager.grantRole(DEPOSIT_REWARD_ROLE, owner.address) + await pnt.approve(await rewardsManager.getAddress(), _amount) + const pntOwnerBalancePre = await pnt.balanceOf(owner.address) + const pntRewardsManagerBalancePre = await pnt.balanceOf(await rewardsManager.getAddress()) + const depositedRewards = await rewardsManager.depositedAmountByEpoch(_epoch) + await rewardsManager.depositForEpoch(_epoch, _amount) + const pntOwnerBalancePost = await pnt.balanceOf(owner.address) + const pntRewardsManagerBalancePost = await pnt.balanceOf(await rewardsManager.getAddress()) + expect(pntOwnerBalancePost).to.be.eq(pntOwnerBalancePre - _amount) + expect(pntRewardsManagerBalancePost).to.be.eq(pntRewardsManagerBalancePre + _amount) + expect(await rewardsManager.depositedAmountByEpoch(_epoch)).to.be.eq(depositedRewards + _amount) + } + + const assertDaoPntBalances = async (_expected) => + expect(await Promise.all(pntHolders.map((_staker) => daoPnt.balanceOf(_staker.address)))).to.be.eql( + _expected.map((_val) => ethers.parseUnits(_val)) + ) + + const assertPntBalances = async (_expected) => + expect(await Promise.all(pntHolders.map((_staker) => pnt.balanceOf(_staker.address)))).to.be.eql( + _expected.map((_val) => ethers.parseUnits(_val)) + ) + + const assertLockedRewardForEpoch = async (_epoch, _expected) => + expect( + await Promise.all(pntHolders.map((_staker) => rewardsManager.lockedRewardByEpoch(_epoch, _staker.address))) + ).to.be.eql(_expected.map((_val) => ethers.parseUnits(_val))) + + const setStakersVoteState = async (_voteId, _states) => + Promise.all( + R.zip(pntHolders, _states).map(([holder, status]) => + dandelionVoting.setTestVoteState(_voteId, holder.address, status) + ) + ) + beforeEach(async () => { const rpc = config.networks.hardhat.forking.url const blockToForkFrom = config.networks.hardhat.forking.blockNumber @@ -58,6 +95,7 @@ describe('RewardsManager', () => { const EpochsManager = await ethers.getContractFactory('EpochsManager') const TestToken = await ethers.getContractFactory('TestToken') const MockDandelionVotingContract = await ethers.getContractFactory('MockDandelionVotingContract') + const ERC20 = await ethers.getContractFactory('ERC20') const signers = await ethers.getSigners() owner = signers[0] @@ -74,6 +112,7 @@ describe('RewardsManager', () => { acl = await ethers.getContractAt(AclAbi, ACL_ADDRESS) tokenManager = await ethers.getContractAt(TokenManagerAbi, TOKEN_MANAGER_ADDRESS) pnt = await TestToken.deploy('PNT', 'PNT') + daoPnt = ERC20.attach(await tokenManager.token()) await Promise.all(pntHolders.map((_holder) => sendPnt(owner, _holder.address, '400000'))) @@ -108,70 +147,60 @@ describe('RewardsManager', () => { }) it('should be possible to deposit tokens', async () => { - const amount = 100n expect(await epochsManager.currentEpoch()).to.be.eq(0) - await rewardsManager.grantRole(DEPOSIT_REWARD_ROLE, owner.address) - await pnt.approve(await rewardsManager.getAddress(), amount) - const pntOwnerBalancePre = await pnt.balanceOf(owner.address) - const pntRewardsManagerBalancePre = await pnt.balanceOf(await rewardsManager.getAddress()) - await rewardsManager.depositForEpoch(0, amount) - const pntOwnerBalancePost = await pnt.balanceOf(owner.address) - const pntRewardsManagerBalancePost = await pnt.balanceOf(await rewardsManager.getAddress()) - expect(pntOwnerBalancePost).to.be.eq(pntOwnerBalancePre - amount) - expect(pntRewardsManagerBalancePost).to.be.eq(pntRewardsManagerBalancePre + amount) - expect(await rewardsManager.depositedAmountByEpoch(0)).to.be.eq(amount) + await depositRewardsForEpoch(100n, 0) + await time.increase(ONE_DAY) + await depositRewardsForEpoch(200n, 0) + await time.increase(ONE_DAY) + await depositRewardsForEpoch(300n, 1) }) - it('should not be able to register for rewards without voting', async () => { + it('should not be possible to deposit rewards for a previous epoch', async () => { + expect(await epochsManager.currentEpoch()).to.be.eq(0) + await time.increase(ONE_MONTH) + expect(await epochsManager.currentEpoch()).to.be.eq(1) + await expect(depositRewardsForEpoch(300n, 0)).to.be.revertedWithCustomError(rewardsManager, 'InvalidEpoch') + }) + + it('should register and assign rewards correctly', async () => { const amount = (ethers.parseUnits('660000') * 10n) / 100n await setPermission(owner.address, await tokenManager.getAddress(), MINT_ROLE) + + await assertDaoPntBalances(['0', '0', '0', '0']) await tokenManager.mint(pntHolder1.address, ethers.parseUnits('200000')) await tokenManager.mint(pntHolder2.address, ethers.parseUnits('400000')) await tokenManager.mint(pntHolder3.address, ethers.parseUnits('50000')) await tokenManager.mint(pntHolder4.address, ethers.parseUnits('10000')) + await assertDaoPntBalances(['200000', '400000', '50000', '10000']) + await assertPntBalances(['400000', '400000', '400000', '400000']) + expect(await epochsManager.currentEpoch()).to.be.eq(0) - await rewardsManager.grantRole(DEPOSIT_REWARD_ROLE, owner.address) - await pnt.approve(await rewardsManager.getAddress(), amount) - const pntOwnerBalancePre = await pnt.balanceOf(owner.address) - const pntRewardsManagerBalancePre = await pnt.balanceOf(await rewardsManager.getAddress()) - await rewardsManager.depositForEpoch(0, amount) - const pntOwnerBalancePost = await pnt.balanceOf(owner.address) - const pntRewardsManagerBalancePost = await pnt.balanceOf(await rewardsManager.getAddress()) - expect(pntOwnerBalancePost).to.be.eq(pntOwnerBalancePre - amount) - expect(pntRewardsManagerBalancePost).to.be.eq(pntRewardsManagerBalancePre + amount) + await depositRewardsForEpoch(amount, 0) + await time.increase(ONE_DAY) await dandelionVoting.newVote() - await Promise.all( - R.zip(pntHolders, [VOTE_STATUS.YES, VOTE_STATUS.YES, VOTE_STATUS.ABSENT, VOTE_STATUS.ABSENT]).map( - ([holder, status]) => dandelionVoting.setTestVoteState(1, holder.address, status) - ) - ) + await setStakersVoteState(1, [VOTE_STATUS.YES, VOTE_STATUS.YES, VOTE_STATUS.ABSENT, VOTE_STATUS.ABSENT]) await time.increase(ONE_DAY * 4) await dandelionVoting.newVote() - await Promise.all( - R.zip(pntHolders, [VOTE_STATUS.YES, VOTE_STATUS.ABSENT, VOTE_STATUS.YES, VOTE_STATUS.ABSENT]).map( - ([holder, status]) => dandelionVoting.setTestVoteState(2, holder.address, status) - ) - ) + await setStakersVoteState(2, [VOTE_STATUS.YES, VOTE_STATUS.ABSENT, VOTE_STATUS.YES, VOTE_STATUS.ABSENT]) await time.increase(ONE_DAY * 4) await dandelionVoting.newVote() - await Promise.all( - R.zip(pntHolders, [VOTE_STATUS.ABSENT, VOTE_STATUS.ABSENT, VOTE_STATUS.YES, VOTE_STATUS.ABSENT]).map( - ([holder, status]) => dandelionVoting.setTestVoteState(3, holder.address, status) - ) - ) + await setStakersVoteState(3, [VOTE_STATUS.ABSENT, VOTE_STATUS.ABSENT, VOTE_STATUS.YES, VOTE_STATUS.ABSENT]) + await time.increase(ONE_MONTH + ONE_DAY) await expect( rewardsManager .connect(randomGuy) .registerRewardsForEpoch(0, [pntHolder1.address, pntHolder2.address, pntHolder3.address]) ).to.not.be.reverted + await assertLockedRewardForEpoch(0, ['20000', '40000', '5000', '0']) + await assertDaoPntBalances(['220000', '440000', '55000', '10000']) + await expect(rewardsManager.connect(randomGuy).registerRewardsForEpoch(0, [pntHolder3.address, pntHolder4.address])) .to.not.be.reverted - expect(await rewardsManager.lockedRewardByEpoch(0, pntHolder1.address)).to.be.eq(ethers.parseUnits('20000')) - expect(await rewardsManager.lockedRewardByEpoch(0, pntHolder2.address)).to.be.eq(ethers.parseUnits('40000')) - expect(await rewardsManager.lockedRewardByEpoch(0, pntHolder3.address)).to.be.eq(ethers.parseUnits('5000')) - expect(await rewardsManager.lockedRewardByEpoch(0, pntHolder4.address)).to.be.eq(ethers.parseUnits('0')) + await assertLockedRewardForEpoch(0, ['20000', '40000', '5000', '0']) + await assertDaoPntBalances(['220000', '440000', '55000', '10000']) + await expect(rewardsManager.connect(pntHolder1).claimRewardByEpoch(0)).to.be.revertedWithCustomError( rewardsManager, 'TooEarly' @@ -184,6 +213,10 @@ describe('RewardsManager', () => { rewardsManager, 'NothingToClaim' ) + await assertLockedRewardForEpoch(0, ['0', '40000', '5000', '0']) + await assertDaoPntBalances(['200000', '440000', '55000', '10000']) + await assertPntBalances(['420000', '400000', '400000', '400000']) + await time.increase(ONE_MONTH) await expect(rewardsManager.connect(pntHolder2).claimRewardByEpoch(0)) .to.emit(pnt, 'Transfer') @@ -192,5 +225,8 @@ describe('RewardsManager', () => { rewardsManager, 'NothingToClaim' ) + await assertLockedRewardForEpoch(0, ['0', '0', '5000', '0']) + await assertDaoPntBalances(['200000', '400000', '55000', '10000']) + await assertPntBalances(['420000', '440000', '400000', '400000']) }) }) From dc228f94d2c22f899e8c6d9e37b19d91858630ca Mon Sep 17 00:00:00 2001 From: Alain Olivier Date: Wed, 14 Feb 2024 13:29:24 +0100 Subject: [PATCH 08/15] refactor(rewards-manager): rename timestamp-related variables --- contracts/core/RewardsManager.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/core/RewardsManager.sol b/contracts/core/RewardsManager.sol index ad9abce..3a7437d 100644 --- a/contracts/core/RewardsManager.sol +++ b/contracts/core/RewardsManager.sol @@ -108,9 +108,9 @@ contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, Acce function _getEpochTimestamps(uint16 epoch) private view returns (uint256, uint256) { uint256 epochDuration = IEpochsManager(epochsManager).epochDuration(); uint256 startFirstEpochTimestamp = IEpochsManager(epochsManager).startFirstEpochTimestamp(); - uint256 epochStartDate = startFirstEpochTimestamp + (epoch * epochDuration); - uint256 epochEndDate = epochStartDate + epochDuration - 1; - return (epochStartDate, epochEndDate); + uint256 epochStartTimestamp = startFirstEpochTimestamp + (epoch * epochDuration); + uint256 epochEndTimestamp = epochStartTimestamp + epochDuration - 1; + return (epochStartTimestamp, epochEndTimestamp); } function _getVotesAndBalancesForEpoch( @@ -122,7 +122,7 @@ contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, Acce uint256 numberOfVotes = votingContract.votesLength(); uint64 voteDuration = votingContract.duration(); - (uint256 epochStartDate, uint256 epochEndDate) = _getEpochTimestamps(epoch); + (uint256 epochStartTimestamp, uint256 epochEndTimestamp) = _getEpochTimestamps(epoch); uint64 lastVoteSnapshotBlock; uint256 supply; @@ -131,9 +131,9 @@ contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, Acce uint256[] memory amounts = new uint256[](stakers.length); for (uint256 voteId = numberOfVotes; voteId >= 1; voteId--) { - (, , uint64 startDate, , uint64 snapshotBlock, , , , , , ) = votingContract.getVote(voteId); - uint64 voteEndDate = startDate + voteDuration; - if (voteEndDate >= epochStartDate && voteEndDate <= epochEndDate) { + (, , uint64 startTimestamp, , uint64 snapshotBlock, , , , , , ) = votingContract.getVote(voteId); + uint64 voteEndTimestamp = startTimestamp + voteDuration; + if (voteEndTimestamp >= epochStartTimestamp && voteEndTimestamp <= epochEndTimestamp) { if (lastVoteSnapshotBlock == 0) { lastVoteSnapshotBlock = snapshotBlock; supply = minime.totalSupplyAt(lastVoteSnapshotBlock); From 52ce1fee899fd6eb04aff941ca43f4e93a10f7e7 Mon Sep 17 00:00:00 2001 From: Alain Olivier Date: Wed, 14 Feb 2024 19:26:54 +0100 Subject: [PATCH 09/15] fix(rewards-manager): fix unclaimableAmountByEpoch accounting --- contracts/core/RewardsManager.sol | 13 ++++---- test/RewardsManager.test.js | 55 ++++++++++++++++++++++++++++--- test/roles.js | 3 +- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/contracts/core/RewardsManager.sol b/contracts/core/RewardsManager.sol index 3a7437d..19b7e01 100644 --- a/contracts/core/RewardsManager.sol +++ b/contracts/core/RewardsManager.sol @@ -141,12 +141,8 @@ contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, Acce for (uint256 i = 0; i < stakers.length; i++) { if ( !hasVoted[i] && - votingContract.getVoterState(voteId, stakers[i]) != IDandelionVoting.VoterState.Absent - ) { - hasVoted[i] = true; - uint256 balance = minime.balanceOfAt(stakers[i], lastVoteSnapshotBlock); - amounts[i] = (depositedAmountByEpoch[epoch] * balance) / supply; - } + (votingContract.getVoterState(voteId, stakers[i]) != IDandelionVoting.VoterState.Absent) + ) hasVoted[i] = true; } } } @@ -155,6 +151,11 @@ contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, Acce revert Errors.NoVoteInEpoch(); } + for (uint256 i = 0; i < stakers.length; i++) { + uint256 balance = minime.balanceOfAt(stakers[i], lastVoteSnapshotBlock); + amounts[i] = (depositedAmountByEpoch[epoch] * balance) / supply; + } + return (hasVoted, amounts); } } diff --git a/test/RewardsManager.test.js b/test/RewardsManager.test.js index 269d7e9..89b8628 100644 --- a/test/RewardsManager.test.js +++ b/test/RewardsManager.test.js @@ -15,7 +15,7 @@ const { PNT_MAX_TOTAL_SUPPLY, VOTE_STATUS } = require('./constants') -const { DEPOSIT_REWARD_ROLE, MINT_ROLE, BURN_ROLE } = require('./roles') +const { DEPOSIT_REWARD_ROLE, MINT_ROLE, BURN_ROLE, WITHDRAW_ROLE } = require('./roles') const { hardhatReset } = require('./utils/hardhat-reset') describe('RewardsManager', () => { @@ -86,6 +86,13 @@ describe('RewardsManager', () => { ) ) + const mintDaoPnt = async (_amounts) => + Promise.all( + R.zip(pntHolders, _amounts).map(([_holder, _amount]) => + tokenManager.mint(_holder.address, ethers.parseUnits(_amount)) + ) + ) + beforeEach(async () => { const rpc = config.networks.hardhat.forking.url const blockToForkFrom = config.networks.hardhat.forking.blockNumber @@ -157,6 +164,7 @@ describe('RewardsManager', () => { it('should not be possible to deposit rewards for a previous epoch', async () => { expect(await epochsManager.currentEpoch()).to.be.eq(0) + await depositRewardsForEpoch(300n, 0) await time.increase(ONE_MONTH) expect(await epochsManager.currentEpoch()).to.be.eq(1) await expect(depositRewardsForEpoch(300n, 0)).to.be.revertedWithCustomError(rewardsManager, 'InvalidEpoch') @@ -167,10 +175,8 @@ describe('RewardsManager', () => { await setPermission(owner.address, await tokenManager.getAddress(), MINT_ROLE) await assertDaoPntBalances(['0', '0', '0', '0']) - await tokenManager.mint(pntHolder1.address, ethers.parseUnits('200000')) - await tokenManager.mint(pntHolder2.address, ethers.parseUnits('400000')) - await tokenManager.mint(pntHolder3.address, ethers.parseUnits('50000')) - await tokenManager.mint(pntHolder4.address, ethers.parseUnits('10000')) + // mint daoPNT to simulate staking + await mintDaoPnt(['200000', '400000', '50000', '10000']) await assertDaoPntBalances(['200000', '400000', '50000', '10000']) await assertPntBalances(['400000', '400000', '400000', '400000']) @@ -195,11 +201,13 @@ describe('RewardsManager', () => { ).to.not.be.reverted await assertLockedRewardForEpoch(0, ['20000', '40000', '5000', '0']) await assertDaoPntBalances(['220000', '440000', '55000', '10000']) + expect(await rewardsManager.unclaimableAmountByEpoch(0)).to.be.eq(0) await expect(rewardsManager.connect(randomGuy).registerRewardsForEpoch(0, [pntHolder3.address, pntHolder4.address])) .to.not.be.reverted await assertLockedRewardForEpoch(0, ['20000', '40000', '5000', '0']) await assertDaoPntBalances(['220000', '440000', '55000', '10000']) + expect(await rewardsManager.unclaimableAmountByEpoch(0)).to.be.eq(ethers.parseUnits('1000')) await expect(rewardsManager.connect(pntHolder1).claimRewardByEpoch(0)).to.be.revertedWithCustomError( rewardsManager, @@ -228,5 +236,42 @@ describe('RewardsManager', () => { await assertLockedRewardForEpoch(0, ['0', '0', '5000', '0']) await assertDaoPntBalances(['200000', '400000', '55000', '10000']) await assertPntBalances(['420000', '440000', '400000', '400000']) + + // withdraw unclaimable rewards + await expect(rewardsManager.connect(randomGuy).withdrawUnclaimableRewardsForEpoch(0)).to.be.revertedWith( + `AccessControl: account ${randomGuy.address.toLowerCase()} is missing role ${WITHDRAW_ROLE}` + ) + await rewardsManager.grantRole(WITHDRAW_ROLE, owner.address) + const ownerBalancePre = await pnt.balanceOf(owner.address) + await rewardsManager.connect(owner).withdrawUnclaimableRewardsForEpoch(0) + expect(await pnt.balanceOf(owner.address)).to.be.eq(ownerBalancePre + ethers.parseUnits('1000')) + await expect(rewardsManager.connect(owner).withdrawUnclaimableRewardsForEpoch(0)).to.be.revertedWithCustomError( + rewardsManager, + 'NothingToWithdraw' + ) + }) + + it('should not register anything if there is no vote in the epoch', async () => { + const amount = (ethers.parseUnits('660000') * 10n) / 100n + await setPermission(owner.address, await tokenManager.getAddress(), MINT_ROLE) + + await assertDaoPntBalances(['0', '0', '0', '0']) + // mint daoPNT to simulate staking + await mintDaoPnt(['200000', '400000', '50000', '10000']) + await assertDaoPntBalances(['200000', '400000', '50000', '10000']) + await assertPntBalances(['400000', '400000', '400000', '400000']) + + expect(await epochsManager.currentEpoch()).to.be.eq(0) + await depositRewardsForEpoch(amount, 0) + + await time.increase(ONE_MONTH + ONE_DAY) + await expect( + rewardsManager + .connect(randomGuy) + .registerRewardsForEpoch(0, [pntHolder1.address, pntHolder2.address, pntHolder3.address]) + ).to.be.revertedWithCustomError(rewardsManager, 'NoVoteInEpoch') + await assertLockedRewardForEpoch(0, ['0', '0', '0', '0']) + await assertDaoPntBalances(['200000', '400000', '50000', '10000']) + expect(await rewardsManager.unclaimableAmountByEpoch(0)).to.be.eq(0) }) }) diff --git a/test/roles.js b/test/roles.js index e1cb075..fc9cb6a 100644 --- a/test/roles.js +++ b/test/roles.js @@ -24,5 +24,6 @@ module.exports = { TRANSFER_ROLE: getRole('TRANSFER_ROLE'), UPDATE_GUARDIAN_REGISTRATION_ROLE: getRole('UPDATE_GUARDIAN_REGISTRATION_ROLE'), UPGRADE_ROLE: getRole('UPGRADE_ROLE'), - DEPOSIT_REWARD_ROLE: getRole('DEPOSIT_REWARD_ROLE') + DEPOSIT_REWARD_ROLE: getRole('DEPOSIT_REWARD_ROLE'), + WITHDRAW_ROLE: getRole('WITHDRAW_ROLE') } From 0c3624cb493a19f0b6e842dc9fe288fe8a6604a6 Mon Sep 17 00:00:00 2001 From: Alain Olivier Date: Thu, 15 Feb 2024 09:44:42 +0100 Subject: [PATCH 10/15] ci: update GitHub Actions packages --- .github/workflows/run-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-ci.yml b/.github/workflows/run-ci.yml index 6f42ecd..9e9054e 100644 --- a/.github/workflows/run-ci.yml +++ b/.github/workflows/run-ci.yml @@ -10,11 +10,11 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js 20.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20.x cache: 'npm' From 72ab73efa06ad4f0fd0c53fe059c9be4b4fd76bc Mon Sep 17 00:00:00 2001 From: Alain Olivier Date: Thu, 15 Feb 2024 10:22:48 +0100 Subject: [PATCH 11/15] refactor(tasks): remove commented lines --- tasks/deploy-dao.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tasks/deploy-dao.js b/tasks/deploy-dao.js index bdbfaa3..243a7a4 100644 --- a/tasks/deploy-dao.js +++ b/tasks/deploy-dao.js @@ -30,7 +30,6 @@ const deploy = async (_args, _hre) => { console.info('StakingManager ...') let stakingManager if (STAKING_MANAGER) { - // await _hre.upgrades.upgradeProxy(STAKING_MANAGER, StakingManager) stakingManager = StakingManager.attach(STAKING_MANAGER) } else { stakingManager = await _hre.upgrades.deployProxy( @@ -47,7 +46,6 @@ const deploy = async (_args, _hre) => { console.info('StakingManager LM ...') let stakingManagerLM if (STAKING_MANAGER_LM) { - // await _hre.upgrades.upgradeProxy(STAKING_MANAGER_LM, StakingManagerPermissioned) stakingManagerLM = StakingManagerPermissioned.attach(STAKING_MANAGER_LM) } else { stakingManagerLM = await _hre.upgrades.deployProxy( @@ -64,7 +62,6 @@ const deploy = async (_args, _hre) => { console.info('StakingManager RM ...') let stakingManagerRM if (STAKING_MANAGER_RM) { - // await _hre.upgrades.upgradeProxy(STAKING_MANAGER_RM, StakingManagerPermissioned) stakingManagerRM = StakingManagerPermissioned.attach(STAKING_MANAGER_RM) } else { stakingManagerRM = await _hre.upgrades.deployProxy( @@ -81,7 +78,6 @@ const deploy = async (_args, _hre) => { console.info('EpochsManager ...') let epochsManager if (EPOCHS_MANAGER) { - // await _hre.upgrades.upgradeProxy(EpochsManager, EpochsManager) epochsManager = EpochsManager.attach(EPOCHS_MANAGER) } else { epochsManager = await _hre.upgrades.deployProxy( @@ -98,7 +94,6 @@ const deploy = async (_args, _hre) => { console.info('LendingManager ...') let lendingManager if (LENDING_MANAGER) { - // await _hre.upgrades.upgradeProxy(LENDING_MANAGER, LendingManager) lendingManager = LendingManager.attach(LENDING_MANAGER) } else { lendingManager = await _hre.upgrades.deployProxy( @@ -122,7 +117,6 @@ const deploy = async (_args, _hre) => { console.info('RegistrationManager ...') let registrationManager if (REGISTRATION_MANAGER) { - // await _hre.upgrades.upgradeProxy(REGISTRATION_MANAGER, RegistrationManager) registrationManager = RegistrationManager.attach(REGISTRATION_MANAGER) } else { registrationManager = await _hre.upgrades.deployProxy( @@ -145,7 +139,6 @@ const deploy = async (_args, _hre) => { console.info('FeesManager ...') let feesManager if (FEES_MANAGER) { - // await _hre.upgrades.upgradeProxy(FEES_MANAGER, FeesManager) feesManager = FeesManager.attach(FEES_MANAGER) } else { feesManager = await _hre.upgrades.deployProxy( From 1a1dfcf10acf6b1582b8c9147cdcdb70948739ea Mon Sep 17 00:00:00 2001 From: Alain Olivier Date: Thu, 15 Feb 2024 10:23:28 +0100 Subject: [PATCH 12/15] feat(tasks): add RewardsManager deployment --- tasks/config.js | 3 +-- tasks/deploy-dao.js | 29 +++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/tasks/config.js b/tasks/config.js index a990e85..6fd094b 100644 --- a/tasks/config.js +++ b/tasks/config.js @@ -1,4 +1,4 @@ -const constants = { +module.exports = { ACL_ADDRESS: '0x50b2b8e429cB51bD43cD3E690e5BEB9eb674f6d7', DANDELION_VOTING_ADDRESS: '0x0cf759bcCfEf5f322af58ADaE2D28885658B5e02', EPOCH_DURATION: 60 * 60 * 24 * 30, @@ -25,4 +25,3 @@ const constants = { REGISTRATION_MANAGER: '0x08342a325630bE00F55A7Bc5dD64D342B1D3d23D', FEES_MANAGER: '0x053b3d59F06601dF87D9EdD24CB2a81FAE93405f' } -module.exports = constants diff --git a/tasks/deploy-dao.js b/tasks/deploy-dao.js index 243a7a4..f7a4377 100644 --- a/tasks/deploy-dao.js +++ b/tasks/deploy-dao.js @@ -16,7 +16,8 @@ const { EPOCHS_MANAGER, LENDING_MANAGER, REGISTRATION_MANAGER, - FEES_MANAGER + FEES_MANAGER, + REWARDS_MANAGER } = require('./config') const deploy = async (_args, _hre) => { @@ -26,6 +27,7 @@ const deploy = async (_args, _hre) => { const LendingManager = await _hre.ethers.getContractFactory('LendingManager') const RegistrationManager = await _hre.ethers.getContractFactory('RegistrationManager') const FeesManager = await _hre.ethers.getContractFactory('FeesManager') + const RewardsManager = await _hre.ethers.getContractFactory('RewardsManager') console.info('StakingManager ...') let stakingManager @@ -158,6 +160,28 @@ const deploy = async (_args, _hre) => { } console.info('FeesManager:', await feesManager.getAddress()) + console.info('RewardsManager ...') + let rewardsManager + if (REWARDS_MANAGER) { + rewardsManager = RewardsManager.attach(REWARDS_MANAGER) + } else { + rewardsManager = await _hre.upgrades.deployProxy( + RewardsManager, + [ + await epochsManager.getAddress(), + DANDELION_VOTING_ADDRESS, + PNT_ON_GNOSIS_ADDRESS, + TOKEN_MANAGER_ADDRESS, + PNT_MAX_TOTAL_SUPPLY + ], + { + initializer: 'initialize', + kind: 'uups' + } + ) + } + console.info('RewardsManager:', await rewardsManager.getAddress()) + console.log( JSON.stringify({ stakingManager: await stakingManager.getAddress(), @@ -166,7 +190,8 @@ const deploy = async (_args, _hre) => { epochsManager: await epochsManager.getAddress(), lendingManager: await lendingManager.getAddress(), registrationManager: await registrationManager.getAddress(), - feesManager: await feesManager.getAddress() + feesManager: await feesManager.getAddress(), + rewardsManager: await rewardsManager.getAddress() }) ) } From effb8664d65efdc185fcb1248b811438c40b16a7 Mon Sep 17 00:00:00 2001 From: Alain Olivier Date: Thu, 15 Feb 2024 10:35:14 +0100 Subject: [PATCH 13/15] chore: add .secretlintignore --- .secretlintignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .secretlintignore diff --git a/.secretlintignore b/.secretlintignore new file mode 100644 index 0000000..63b7609 --- /dev/null +++ b/.secretlintignore @@ -0,0 +1 @@ +.openzeppelin/*.json From d9c9a253fc841acd008e4f046357ef8c92566a9d Mon Sep 17 00:00:00 2001 From: Alain Olivier Date: Thu, 15 Feb 2024 10:35:32 +0100 Subject: [PATCH 14/15] chore: update references with deployed rewards manager --- .openzeppelin/unknown-100.json | 282 +++++++++++++++++++++++++++++++++ tasks/config.js | 3 +- 2 files changed, 284 insertions(+), 1 deletion(-) diff --git a/.openzeppelin/unknown-100.json b/.openzeppelin/unknown-100.json index 80a847b..6f9ace7 100644 --- a/.openzeppelin/unknown-100.json +++ b/.openzeppelin/unknown-100.json @@ -70,6 +70,11 @@ "address": "0x053b3d59F06601dF87D9EdD24CB2a81FAE93405f", "txHash": "0x251d63ca89dc90f4062c7f9db269731bde99468c7107234390aec9497b2c0acd", "kind": "uups" + }, + { + "address": "0x2ec44F9F31a55b52b3c1fF98647E38d63f829fb7", + "txHash": "0x1fcea19096ed58ac561fa31a188cc70df8661eef7dca3532e5cb0cf9f28741a2", + "kind": "uups" } ], "impls": { @@ -3128,6 +3133,283 @@ } } } + }, + "7e4179853961ebc2114609e808cea6b18129cd4846f5650f236c85a26207cdab": { + "address": "0xd29BEf4B60b365C243e1350476c9822f25476ab1", + "txHash": "0x84f177523983312ce6a9d38eedda36a2c44f58952585d11417e6a42265ea05e9", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_roles", + "offset": 0, + "slot": "201", + "type": "t_mapping(t_bytes32,t_struct(RoleData)169_storage)", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:57" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)49_storage", + "contract": "AccessControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:260" + }, + { + "label": "_roleMembers", + "offset": 0, + "slot": "251", + "type": "t_mapping(t_bytes32,t_struct(AddressSet)3819_storage)", + "contract": "AccessControlEnumerableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol:17" + }, + { + "label": "__gap", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)49_storage", + "contract": "AccessControlEnumerableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol:76" + }, + { + "label": "epochsManager", + "offset": 0, + "slot": "301", + "type": "t_address", + "contract": "RewardsManager", + "src": "contracts/core/RewardsManager.sol:22" + }, + { + "label": "dandelionVoting", + "offset": 0, + "slot": "302", + "type": "t_address", + "contract": "RewardsManager", + "src": "contracts/core/RewardsManager.sol:23" + }, + { + "label": "maxTotalSupply", + "offset": 0, + "slot": "303", + "type": "t_uint256", + "contract": "RewardsManager", + "src": "contracts/core/RewardsManager.sol:24" + }, + { + "label": "token", + "offset": 0, + "slot": "304", + "type": "t_address", + "contract": "RewardsManager", + "src": "contracts/core/RewardsManager.sol:25" + }, + { + "label": "tokenManager", + "offset": 0, + "slot": "305", + "type": "t_address", + "contract": "RewardsManager", + "src": "contracts/core/RewardsManager.sol:26" + }, + { + "label": "depositedAmountByEpoch", + "offset": 0, + "slot": "306", + "type": "t_mapping(t_uint16,t_uint256)", + "contract": "RewardsManager", + "src": "contracts/core/RewardsManager.sol:28" + }, + { + "label": "claimedAmountByEpoch", + "offset": 0, + "slot": "307", + "type": "t_mapping(t_uint16,t_uint256)", + "contract": "RewardsManager", + "src": "contracts/core/RewardsManager.sol:29" + }, + { + "label": "unclaimableAmountByEpoch", + "offset": 0, + "slot": "308", + "type": "t_mapping(t_uint16,t_uint256)", + "contract": "RewardsManager", + "src": "contracts/core/RewardsManager.sol:30" + }, + { + "label": "lockedRewardByEpoch", + "offset": 0, + "slot": "309", + "type": "t_mapping(t_uint16,t_mapping(t_address,t_uint256))", + "contract": "RewardsManager", + "src": "contracts/core/RewardsManager.sol:31" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(AddressSet)3819_storage)": { + "label": "mapping(bytes32 => struct EnumerableSetUpgradeable.AddressSet)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)169_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint16,t_mapping(t_address,t_uint256))": { + "label": "mapping(uint16 => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint16,t_uint256)": { + "label": "mapping(uint16 => uint256)", + "numberOfBytes": "32" + }, + "t_struct(AddressSet)3819_storage": { + "label": "struct EnumerableSetUpgradeable.AddressSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)3504_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(RoleData)169_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "members", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Set)3504_storage": { + "label": "struct EnumerableSetUpgradeable.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } } } } diff --git a/tasks/config.js b/tasks/config.js index 6fd094b..913f1e6 100644 --- a/tasks/config.js +++ b/tasks/config.js @@ -23,5 +23,6 @@ module.exports = { EPOCHS_MANAGER: '0xFDD7d2f23F771F05C6CEbFc9f9bC2A771FAE302e', LENDING_MANAGER: '0xEf3A54f764F58848e66BaDc427542b44C44b5553', REGISTRATION_MANAGER: '0x08342a325630bE00F55A7Bc5dD64D342B1D3d23D', - FEES_MANAGER: '0x053b3d59F06601dF87D9EdD24CB2a81FAE93405f' + FEES_MANAGER: '0x053b3d59F06601dF87D9EdD24CB2a81FAE93405f', + REWARDS_MANAGER: '0x2ec44F9F31a55b52b3c1fF98647E38d63f829fb7' } From 11d3abf37af6dfdeec29528e4af9af4ed703aac1 Mon Sep 17 00:00:00 2001 From: Alain Olivier Date: Thu, 15 Feb 2024 15:02:45 +0100 Subject: [PATCH 15/15] docs(rewards-manager): add doc comments to interface --- contracts/core/RewardsManager.sol | 4 ++++ contracts/interfaces/IRewardsManager.sol | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/contracts/core/RewardsManager.sol b/contracts/core/RewardsManager.sol index 19b7e01..5f7515e 100644 --- a/contracts/core/RewardsManager.sol +++ b/contracts/core/RewardsManager.sol @@ -49,6 +49,7 @@ contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, Acce maxTotalSupply = _maxTotalSupply; } + /// @inheritdoc IRewardsManager function claimRewardByEpoch(uint16 epoch) external { address sender = _msgSender(); uint16 currentEpoch = IEpochsManager(epochsManager).currentEpoch(); @@ -62,6 +63,7 @@ contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, Acce } else revert Errors.NothingToClaim(); } + /// @inheritdoc IRewardsManager function depositForEpoch(uint16 epoch, uint256 amount) external onlyRole(Roles.DEPOSIT_REWARD_ROLE) { address sender = _msgSender(); uint16 currentEpoch = IEpochsManager(epochsManager).currentEpoch(); @@ -70,6 +72,7 @@ contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, Acce depositedAmountByEpoch[epoch] += amount; } + /// @inheritdoc IRewardsManager function registerRewardsForEpoch(uint16 epoch, address[] calldata stakers) external { uint16 currentEpoch = IEpochsManager(epochsManager).currentEpoch(); if (epoch >= currentEpoch) revert Errors.InvalidEpoch(); @@ -88,6 +91,7 @@ contract RewardsManager is IRewardsManager, Initializable, UUPSUpgradeable, Acce } } + /// @inheritdoc IRewardsManager function withdrawUnclaimableRewardsForEpoch(uint16 epoch) external onlyRole(Roles.WITHDRAW_ROLE) { if (unclaimableAmountByEpoch[epoch] > 0) { address sender = _msgSender(); diff --git a/contracts/interfaces/IRewardsManager.sol b/contracts/interfaces/IRewardsManager.sol index eead272..3b8167c 100644 --- a/contracts/interfaces/IRewardsManager.sol +++ b/contracts/interfaces/IRewardsManager.sol @@ -9,9 +9,29 @@ pragma solidity ^0.8.17; * @notice */ interface IRewardsManager { + /* + * Allows a staker to claim their rewards for a specific epoch. + * @param {uint16} epoch - The epoch number for which rewards are being claimed. + */ function claimRewardByEpoch(uint16 epoch) external; + /* + * Allows to deposit rewards that will be distributed for a specific epoch. + * @param {uint16} epoch - The epoch number for which the staker is depositing tokens. + * @param {uint256} amount - The amount of tokens the staker is depositing. + */ function depositForEpoch(uint16 epoch, uint256 amount) external; + /* + * Allows to register rewards for a specific epoch for a set of stakers. + * @param {uint16} epoch - The epoch number for which rewards are being registered. + * @param {address[]} stakers - An array of addresses representing the stakers to register rewards for. + */ function registerRewardsForEpoch(uint16 epoch, address[] calldata stakers) external; + + /* + * Allows to withdraw unclaimable rewards for a specific epoch. + * @param {uint16} epoch - The epoch number for which unclaimable rewards are being withdrawn. + */ + function withdrawUnclaimableRewardsForEpoch(uint16 epoch) external; }