From 6559556e17a443c15efb7b66ef43f820a348e446 Mon Sep 17 00:00:00 2001 From: rkolpakov Date: Wed, 14 Dec 2022 21:23:56 +0400 Subject: [PATCH] feat: add lido rewards distribution --- contracts/0.4.24/Lido.sol | 36 +++-- test/0.4.24/lido.rewards-distribution.test.js | 139 ++++++++++++++++++ 2 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 test/0.4.24/lido.rewards-distribution.test.js diff --git a/contracts/0.4.24/Lido.sol b/contracts/0.4.24/Lido.sol index c58bd5338..6e6b07f9b 100644 --- a/contracts/0.4.24/Lido.sol +++ b/contracts/0.4.24/Lido.sol @@ -744,20 +744,36 @@ contract Lido is ILido, StETH, AragonApp { // token shares. address stakingRouterAddress = getStakingRouter(); + ( + address[] memory moduleAddresses, + uint256[] memory moduleShares, + uint256[] memory moduleFees, + uint256[] memory moduleTreasuryFees + ) = IStakingRouter(stakingRouterAddress).getSharesTable(); + + address treasury = getTreasury(); + uint256 rewards2mint = 0; + uint256[] memory moduleRewards = new uint256[](moduleAddresses.length); + + for (uint256 i = 0; i < moduleAddresses.length; i++) { + uint256 moduleShare = _totalRewards * moduleShares[i] / TOTAL_BASIS_POINTS; + + moduleRewards[i] = moduleShare * moduleFees[i] / TOTAL_BASIS_POINTS; + rewards2mint += moduleShare * moduleTreasuryFees[i] / TOTAL_BASIS_POINTS + moduleRewards[i]; + } + uint256 shares2mint = rewards2mint.mul(_getTotalShares()).div(_getTotalPooledEther().sub(rewards2mint)); - // address modulefee treasuryfee - - // (uint256 shares2mint, uint256 totalKeys, uint256[] memory moduleKeys) = IStakingRouter(stakingRouterAddress).calculateShares2Mint( - // _totalRewards - // ); + _mintShares(address(this), shares2mint); - // // Mint the calculated amount of shares to this contract address. This will reduce the - // // balances of the holders, as if the fee was taken in parts from each of them. - // _mintShares(stakingRouterAddress, shares2mint); + for (uint256 j = 0; j < moduleAddresses.length; j++) { + uint256 moduleRewardInShares = getSharesByPooledEth(moduleRewards[j]); + shares2mint -= moduleRewardInShares; - // //distribute shares - // IStakingRouter(stakingRouterAddress).distributeShares(shares2mint, totalKeys, moduleKeys); + _transferShares(address(this), moduleAddresses[j], moduleRewardInShares); + } + + _transferShares(address(this), treasury, shares2mint); } /** diff --git a/test/0.4.24/lido.rewards-distribution.test.js b/test/0.4.24/lido.rewards-distribution.test.js new file mode 100644 index 000000000..07b3c3d9f --- /dev/null +++ b/test/0.4.24/lido.rewards-distribution.test.js @@ -0,0 +1,139 @@ +const { assert } = require('chai') +const { newDao, newApp } = require('./helpers/dao') +const { assertBn, assertRevert, assertEvent } = require('@aragon/contract-helpers-test/src/asserts') +const { ZERO_ADDRESS, bn } = require('@aragon/contract-helpers-test') + +const NodeOperatorsRegistry = artifacts.require('NodeOperatorsRegistryMock') + +const Lido = artifacts.require('LidoMock.sol') +const OracleMock = artifacts.require('OracleMock.sol') +const DepositContractMock = artifacts.require('DepositContractMock.sol') +const StakingRouter = artifacts.require('StakingRouter.sol') +const ModuleSolo = artifacts.require('ModuleSolo.sol') + +const TOTAL_BASIS_POINTS = 10000 +const ETH = (value) => web3.utils.toWei(value + '', 'ether') + +contract('Lido', ([appManager, voting, user2, depositor]) => { + let appBase, nodeOperatorsRegistryBase, app, oracle, depositContract, curatedModule, stakingRouter, soloModule + let treasuryAddr + let dao, acl + + before('deploy base app', async () => { + // Deploy the app's base contract. + appBase = await Lido.new() + oracle = await OracleMock.new() + depositContract = await DepositContractMock.new() + nodeOperatorsRegistryBase = await NodeOperatorsRegistry.new() + }) + + beforeEach('deploy dao and app', async () => { + ;({ dao, acl } = await newDao(appManager)) + + // Instantiate a proxy for the app, using the base contract as its logic implementation. + let proxyAddress = await newApp(dao, 'lido', appBase.address, appManager) + app = await Lido.at(proxyAddress) + + // NodeOperatorsRegistry + proxyAddress = await newApp(dao, 'node-operators-registry', nodeOperatorsRegistryBase.address, appManager) + curatedModule = await NodeOperatorsRegistry.at(proxyAddress) + await curatedModule.initialize(app.address) + + // Set up the app's permissions. + await acl.createPermission(voting, app.address, await app.PAUSE_ROLE(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.RESUME_ROLE(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.MANAGE_FEE(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.MANAGE_WITHDRAWAL_KEY(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.BURN_ROLE(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.MANAGE_PROTOCOL_CONTRACTS_ROLE(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.SET_EL_REWARDS_VAULT_ROLE(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.SET_EL_REWARDS_WITHDRAWAL_LIMIT_ROLE(), appManager, { + from: appManager + }) + await acl.createPermission(voting, app.address, await app.STAKING_PAUSE_ROLE(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.STAKING_CONTROL_ROLE(), appManager, { from: appManager }) + + await acl.createPermission(voting, curatedModule.address, await curatedModule.MANAGE_SIGNING_KEYS(), appManager, { from: appManager }) + await acl.createPermission(voting, curatedModule.address, await curatedModule.ADD_NODE_OPERATOR_ROLE(), appManager, { + from: appManager + }) + await acl.createPermission(voting, curatedModule.address, await curatedModule.SET_NODE_OPERATOR_ACTIVE_ROLE(), appManager, { + from: appManager + }) + await acl.createPermission(voting, curatedModule.address, await curatedModule.SET_NODE_OPERATOR_NAME_ROLE(), appManager, { + from: appManager + }) + await acl.createPermission(voting, curatedModule.address, await curatedModule.SET_NODE_OPERATOR_ADDRESS_ROLE(), appManager, { + from: appManager + }) + await acl.createPermission(voting, curatedModule.address, await curatedModule.SET_NODE_OPERATOR_LIMIT_ROLE(), appManager, { + from: appManager + }) + await acl.createPermission(voting, curatedModule.address, await curatedModule.REPORT_STOPPED_VALIDATORS_ROLE(), appManager, { + from: appManager + }) + await acl.createPermission(depositor, app.address, await app.DEPOSIT_ROLE(), appManager, { from: appManager }) + + // Initialize the app's proxy. + await app.initialize(depositContract.address, oracle.address, curatedModule.address) + + assert((await app.isStakingPaused()) === true) + assert((await app.isStopped()) === true) + await app.resume({ from: voting }) + assert((await app.isStakingPaused()) === false) + assert((await app.isStopped()) === false) + + treasuryAddr = await app.getTreasury() + + await oracle.setPool(app.address) + await depositContract.reset() + }) + + beforeEach('setup staking router', async () => { + stakingRouter = await StakingRouter.new(app.address, depositContract.address) + await app.setStakingRouter(stakingRouter.address) + + soloModule = await ModuleSolo.new(1, app.address, 500, { from: appManager }) + + await stakingRouter.addModule('Curated', curatedModule.address, 0, 500, { from: appManager }) + await curatedModule.setTotalKeys(100, { from: appManager }) + await curatedModule.setTotalUsedKeys(50, { from: appManager }) + await curatedModule.setTotalStoppedKeys(100, { from: appManager }) + + await stakingRouter.addModule('Solo', soloModule.address, 0, 500, { from: appManager }) + await soloModule.setTotalKeys(100, { from: appManager }) + await soloModule.setTotalUsedKeys(50, { from: appManager }) + await soloModule.setTotalStoppedKeys(100, { from: appManager }) + + stakingModules = [curatedModule, soloModule] + }) + + it('Rewards distribution fills treasury', async () => { + const depositAmount = ETH(1) + const treasuryRewards = (depositAmount * 500) / TOTAL_BASIS_POINTS + + await app.submit(ZERO_ADDRESS, { from: user2, value: ETH(32) }) + + const treasuryBalanceBefore = await app.balanceOf(treasuryAddr) + await oracle.reportBeacon(100, 0, depositAmount, { from: appManager }) + + const treasuryBalanceAfter = await app.balanceOf(treasuryAddr) + assertBn(treasuryBalanceBefore.add(bn(treasuryRewards)).sub(bn(1)), treasuryBalanceAfter) + }) + + it('Rewards distribution fills modules', async () => { + const depositAmount = ETH(1) + const { modulesShares } = await stakingRouter.getSharesTable() + const moduleFee = (depositAmount * modulesShares[0]) / TOTAL_BASIS_POINTS + const rewards = (moduleFee * (await soloModule.getFee())) / TOTAL_BASIS_POINTS + + await app.submit(ZERO_ADDRESS, { from: user2, value: ETH(32) }) + + const moduleBalanceBefore = await app.balanceOf(soloModule.address) + + await oracle.reportBeacon(100, 0, depositAmount, { from: appManager }) + + const moduleBalanceAfter = await app.balanceOf(soloModule.address) + assertBn(moduleBalanceBefore.add(bn(rewards).sub(bn(1))), moduleBalanceAfter) + }) +})