From 08d11e93c509d7bdf618c25c06a326b8d263397f Mon Sep 17 00:00:00 2001 From: jaebok Date: Mon, 10 Jan 2022 15:25:20 +0900 Subject: [PATCH 1/6] add setup function and lastUpdateBlock in initialize function --- contracts/IncentivePool.sol | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/contracts/IncentivePool.sol b/contracts/IncentivePool.sol index 97832c5..2f8d89d 100644 --- a/contracts/IncentivePool.sol +++ b/contracts/IncentivePool.sol @@ -49,7 +49,8 @@ contract IncentivePool is IIncentivePool { require(!_initialized, 'AlreadyInitialized'); _initialized = true; lToken = lToken_; - endTimestamp = block.timestamp + 180 * 1 days; + _lastUpdateTimestamp = block.timestamp; + endTimestamp = block.timestamp + 95 * 1 days; } function isClosed() public view returns (bool) { @@ -152,21 +153,45 @@ contract IncentivePool is IIncentivePool { return (_incentiveIndex, _lastUpdateTimestamp); } - function withdrawResidue() external override { - require(msg.sender == _owner, 'onlyAdmin'); + function withdrawResidue() external override onlyOwner { require(isClosed(), 'OnlyClosed'); uint256 residue = IERC20(_incentiveAsset).balanceOf(address(this)); IERC20(_incentiveAsset).safeTransfer(_owner, residue); emit IncentivePoolEnded(); } - modifier onlyMoneyPool { + /** + * @notice Admin can update amount per second + */ + function setAmountPerSecond(uint256 newAmountPerSecond) external override onlyOwner { + _incentiveIndex = getIncentiveIndex(); + + amountPerSecond = newAmountPerSecond; + + emit RewardPerSecondUpdated(newAmountPerSecond); + } + + /** + * @notice Admin can update incentive pool end timestamp + */ + function setEndTimestamp(uint256 newEndTimestamp) external override onlyOwner { + endTimestamp = newEndTimestamp; + + emit IncentiveEndTimestampUpdated(newEndTimestamp); + } + + modifier onlyMoneyPool() { require(msg.sender == address(_moneyPool), 'OnlyMoneyPool'); _; } - modifier onlyLToken { + modifier onlyLToken() { require(msg.sender == address(lToken), 'OnlyLToken'); _; } + + modifier onlyOwner() { + require(msg.sender == _owner, 'onlyAdmin'); + _; + } } From 03af8072b860acbd3f95c2ef2124b0387e910dec Mon Sep 17 00:00:00 2001 From: jaebok Date: Mon, 10 Jan 2022 15:25:38 +0900 Subject: [PATCH 2/6] add incentive pool interface --- contracts/interfaces/IIncentivePool.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/contracts/interfaces/IIncentivePool.sol b/contracts/interfaces/IIncentivePool.sol index 2c4dbc3..a2c149a 100644 --- a/contracts/interfaces/IIncentivePool.sol +++ b/contracts/interfaces/IIncentivePool.sol @@ -10,8 +10,19 @@ interface IIncentivePool { event IncentivePoolEnded(); + event RewardPerSecondUpdated(uint256 newAmountPerSecond); + + event IncentiveEndTimestampUpdated(uint256 newEndTimestamp); + function initializeIncentivePool(address lToken) external; + function setAmountPerSecond(uint256 newAmountPerSecond) external; + + /** + * @notice Admin can update incentive pool end timestamp + */ + function setEndTimestamp(uint256 newEndTimestamp) external; + function updateIncentivePool(address user) external; function beforeTokenTransfer(address from, address to) external; From ffa4e413999e2dbea1db0dc70909ae1889de2ffe Mon Sep 17 00:00:00 2001 From: jaebok Date: Mon, 10 Jan 2022 15:26:10 +0900 Subject: [PATCH 3/6] add update incentive pool test cases --- test/contracts/IncentivePool/update.test.ts | 250 ++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 test/contracts/IncentivePool/update.test.ts diff --git a/test/contracts/IncentivePool/update.test.ts b/test/contracts/IncentivePool/update.test.ts new file mode 100644 index 0000000..d27bcc8 --- /dev/null +++ b/test/contracts/IncentivePool/update.test.ts @@ -0,0 +1,250 @@ +import hre from 'hardhat'; + +import { ethers, waffle } from 'hardhat'; +import ElyfiContracts from '../../types/ElyfiContracts'; +import { BigNumber, utils } from 'ethers'; +import { setupAllContracts } from '../../utils/setup'; +import { RAY } from '../../utils/constants'; +import { getIncentivePoolData, getUserIncentiveData } from '../../utils/Helpers'; +import { expectIncentiveDataAfterDeposit } from '../../utils/Expect'; +import { advanceTimeTo, getTimestamp } from '../../utils/time'; +import IncentivePoolData from '../../types/IncentivePoolData'; +import UserIncentiveData from '../../types/UserIncentiveData'; +import { expect } from 'chai'; +import { IncentivePool, IncentivePool__factory } from '../../../typechain'; +import { testIncentiveAmountPerSecond } from '../../utils/testData'; +import { calculateIncentiveIndex, calculateUserIncentive } from '../../utils/calculations'; +require('../../assertions/equals.ts'); + +describe('', () => { + let elyfiContracts: ElyfiContracts; + let newIncentivePool: IncentivePool; + + const provider = waffle.provider; + const [deployer, account0, account1] = provider.getWallets(); + + const amount = ethers.utils.parseEther('1'); + + beforeEach('', async () => { + elyfiContracts = await setupAllContracts(); + + const incentivePoolFactory = (await ethers.getContractFactory( + 'IncentivePool' + )) as IncentivePool__factory; + + newIncentivePool = await incentivePoolFactory.deploy( + deployer.address, + elyfiContracts.incentiveAsset.address, + testIncentiveAmountPerSecond + ); + + await elyfiContracts.incentiveAsset + .connect(deployer) + .transfer(elyfiContracts.incentivePool.address, utils.parseEther('1000')); + await elyfiContracts.underlyingAsset.connect(deployer).faucet(); + await elyfiContracts.underlyingAsset + .connect(deployer) + .approve(elyfiContracts.moneyPool.address, RAY); + }); + + context('admin functions', async () => { + beforeEach('deposit and time passes', async () => { + await elyfiContracts.moneyPool + .connect(deployer) + .deposit(elyfiContracts.underlyingAsset.address, deployer.address, amount); + await elyfiContracts.moneyPool + .connect(deployer) + .deposit(elyfiContracts.underlyingAsset.address, deployer.address, amount); + }); + + it('admin can update reward amount per second, and index should be updated ', async () => { + const newIncentiveAmountPerSecond = testIncentiveAmountPerSecond.mul(2); + + const incentivePoolDataBefore = await getIncentivePoolData({ + incentivePool: newIncentivePool, + lToken: elyfiContracts.lToken, + incentiveAsset: elyfiContracts.incentiveAsset, + }); + + const tx = await elyfiContracts.incentivePool + .connect(deployer) + .setAmountPerSecond(newIncentiveAmountPerSecond); + + const expectedIncentiveIndexAfterUpdate = calculateIncentiveIndex( + incentivePoolDataBefore, + await getTimestamp(tx) + ); + + const incentivePoolDataAfter = await getIncentivePoolData({ + incentivePool: newIncentivePool, + lToken: elyfiContracts.lToken, + incentiveAsset: elyfiContracts.incentiveAsset, + }); + + expect(tx) + .to.be.emit(elyfiContracts.incentivePool, 'RewardPerSecondUpdated') + .withArgs(newIncentiveAmountPerSecond); + expect(expectedIncentiveIndexAfterUpdate).to.be.equal(incentivePoolDataAfter.incentiveIndex); + expect(await elyfiContracts.incentivePool.amountPerSecond()).to.be.equal( + newIncentiveAmountPerSecond + ); + }); + + it('reverts if general account0 update incentive amount per second', async () => { + const newIncentiveAmountPerSecond = testIncentiveAmountPerSecond.mul(2); + + await expect( + elyfiContracts.incentivePool + .connect(account0) + .setAmountPerSecond(newIncentiveAmountPerSecond) + ).to.be.revertedWith('onlyAdmin'); + }); + + it('admin can update endTimestamp', async () => { + const newEndTimestamp = (await elyfiContracts.incentivePool.endTimestamp()).add(1); + + const tx = await elyfiContracts.incentivePool + .connect(deployer) + .setEndTimestamp(newEndTimestamp); + + expect(tx) + .to.be.emit(elyfiContracts.incentivePool, 'IncentiveEndTimestampUpdated') + .withArgs(newEndTimestamp); + expect(await elyfiContracts.incentivePool.endTimestamp()).to.be.equal(newEndTimestamp); + }); + + it('reverts if general account0 update endTimestamp', async () => { + const newEndTimestamp = (await elyfiContracts.incentivePool.endTimestamp()).add(1); + + await expect( + elyfiContracts.incentivePool.connect(account0).setEndTimestamp(newEndTimestamp) + ).to.be.revertedWith('onlyAdmin'); + }); + }); + + context('update incentivePool', async () => { + let newIncentivePool: IncentivePool; + let newIncentivePoolInitTimestamp: BigNumber; + + beforeEach('', async () => { + const incentivePoolFactory = (await ethers.getContractFactory( + 'IncentivePool' + )) as IncentivePool__factory; + + newIncentivePool = await incentivePoolFactory.deploy( + deployer.address, + elyfiContracts.incentiveAsset.address, + testIncentiveAmountPerSecond + ); + + await elyfiContracts.underlyingAsset + .connect(deployer) + .transfer(account0.address, utils.parseEther('1000')); + await elyfiContracts.underlyingAsset + .connect(deployer) + .transfer(account1.address, utils.parseEther('1000')); + await elyfiContracts.incentiveAsset + .connect(deployer) + .transfer(elyfiContracts.incentivePool.address, RAY); + + await elyfiContracts.underlyingAsset + .connect(account0) + .approve(elyfiContracts.moneyPool.address, RAY); + await elyfiContracts.underlyingAsset + .connect(account1) + .approve(elyfiContracts.moneyPool.address, RAY); + + await elyfiContracts.moneyPool + .connect(account1) + .deposit(elyfiContracts.underlyingAsset.address, account1.address, amount); + + await elyfiContracts.moneyPool + .connect(account0) + .deposit(elyfiContracts.underlyingAsset.address, account0.address, amount); + + await elyfiContracts.moneyPool + .connect(deployer) + .updateIncentivePool(elyfiContracts.underlyingAsset.address, newIncentivePool.address); + const tx = await newIncentivePool + .connect(deployer) + .initializeIncentivePool(elyfiContracts.lToken.address); + + newIncentivePoolInitTimestamp = await getTimestamp(tx); + }); + + it('set property successfully', async () => { + expect(await newIncentivePool.lToken()).to.be.equal(elyfiContracts.lToken.address); + }); + + it('user can accure incentive after update and it should be same as existing pool', async () => { + const userIncentiveDataFromOldPool = await getUserIncentiveData({ + incentivePool: elyfiContracts.incentivePool, + lToken: elyfiContracts.lToken, + incentiveAsset: elyfiContracts.incentiveAsset, + user: account0, + }); + const incentivePoolDataFromOldPool = await getIncentivePoolData({ + incentivePool: elyfiContracts.incentivePool, + lToken: elyfiContracts.lToken, + incentiveAsset: elyfiContracts.incentiveAsset, + }); + + await advanceTimeTo(newIncentivePoolInitTimestamp, newIncentivePoolInitTimestamp.add(100)); + + // Since transactions cannot be executed in the same timestamp, + // timestamp arg for calculateUserIncentive in old pool should be subtracted 2 second + // due to the `updateIncentivePool` and `initializeIncentivePool` in `beforeEach` + const expectedIncentiveInNewPool = calculateUserIncentive( + incentivePoolDataFromOldPool, + userIncentiveDataFromOldPool, + newIncentivePoolInitTimestamp.add(98) + ); + + const rewardInNewPool = await newIncentivePool.getUserIncentive(account0.address); + + expect(expectedIncentiveInNewPool).to.be.equal(rewardInNewPool); + }); + + it('deposit after update incentive pool, and incentive data should be same as expected', async () => { + const userIncentiveDataBefore = await getUserIncentiveData({ + incentivePool: newIncentivePool, + lToken: elyfiContracts.lToken, + incentiveAsset: elyfiContracts.incentiveAsset, + user: account0, + }); + const incentivePoolDataBefore = await getIncentivePoolData({ + incentivePool: newIncentivePool, + lToken: elyfiContracts.lToken, + incentiveAsset: elyfiContracts.incentiveAsset, + }); + const tx = await elyfiContracts.moneyPool + .connect(account0) + .deposit(elyfiContracts.underlyingAsset.address, account0.address, amount); + + const [expectedIncentivePoolData, expectedUserIncentiveData]: [ + IncentivePoolData, + UserIncentiveData + ] = expectIncentiveDataAfterDeposit( + incentivePoolDataBefore, + userIncentiveDataBefore, + await getTimestamp(tx), + amount + ); + + const userIncentiveDataAfter = await getUserIncentiveData({ + incentivePool: newIncentivePool, + lToken: elyfiContracts.lToken, + incentiveAsset: elyfiContracts.incentiveAsset, + user: account0, + }); + const incentivePoolDataAfter = await getIncentivePoolData({ + incentivePool: newIncentivePool, + lToken: elyfiContracts.lToken, + incentiveAsset: elyfiContracts.incentiveAsset, + }); + + expect(expectedIncentivePoolData).to.be.deepEqualWithBigNumber(incentivePoolDataAfter); + expect(expectedUserIncentiveData).to.be.deepEqualWithBigNumber(userIncentiveDataAfter); + }); + }); +}); From 792dffd0a85f42756309d53e7d5fd07adfe8ec03 Mon Sep 17 00:00:00 2001 From: jaebok Date: Mon, 10 Jan 2022 15:26:27 +0900 Subject: [PATCH 4/6] remove abi-exporter --- hardhat.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 219ea35..05d4a7b 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -6,7 +6,7 @@ import '@openzeppelin/hardhat-upgrades'; import '@typechain/hardhat'; import 'hardhat-deploy-ethers'; import 'hardhat-deploy'; -import 'hardhat-abi-exporter'; +// import 'hardhat-abi-exporter'; // import "solidity-coverage" // Gas-reporter's parser dependency makes Warning: // Accessing non-existent property 'INVALID_ALT_NUMBER' of module exports inside circular dependency From a596affb84c9a5fe3b902705cf345cbeedbe4f47 Mon Sep 17 00:00:00 2001 From: "donguk.seo" Date: Tue, 11 Jan 2022 11:01:09 +0900 Subject: [PATCH 5/6] impl v2 incentive --- contracts/IncentivePoolV2.sol | 194 ++++++++++++++++++++++ contracts/interfaces/IIncentivePoolV2.sol | 31 ++++ 2 files changed, 225 insertions(+) create mode 100644 contracts/IncentivePoolV2.sol create mode 100644 contracts/interfaces/IIncentivePoolV2.sol diff --git a/contracts/IncentivePoolV2.sol b/contracts/IncentivePoolV2.sol new file mode 100644 index 0000000..83908fd --- /dev/null +++ b/contracts/IncentivePoolV2.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.3; + +import './libraries/WadRayMath.sol'; +import './interfaces/IIncentivePoolV2.sol'; +import './interfaces/IMoneyPool.sol'; +import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; +import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import 'hardhat/console.sol'; + +contract IncentivePoolV2 is IIncentivePoolV2 { + using WadRayMath for uint256; + using SafeERC20 for IERC20; + + constructor( + IMoneyPool moneyPool, + address incentiveAsset, + address lToken_, + uint256 amountPerSecond_, + uint256 startTimestamp, + uint256 _endTimestamp + ) { + _moneyPool = moneyPool; + _incentiveAsset = incentiveAsset; + amountPerSecond = amountPerSecond_; + _owner = msg.sender; + lToken = lToken_; + _lastUpdateTimestamp = startTimestamp; + endTimestamp = _endTimestamp; + } + + address private _owner; + + IMoneyPool internal _moneyPool; + + address internal _incentiveAsset; + + uint256 internal _incentiveIndex; + + uint256 internal _lastUpdateTimestamp; + + mapping(address => uint256) internal _userIncentiveIndex; + + mapping(address => uint256) internal _accruedIncentive; + + uint256 public amountPerSecond; + + address public lToken; + + uint256 public endTimestamp; + + function isClosed() public view returns (bool) { + if (block.timestamp > endTimestamp) { + return true; + } + return false; + } + + /** + * @notice Update user incentive index and last update timestamp in minting or burining lTokens. + */ + function updateIncentivePool(address user) external override onlyLToken { + _accruedIncentive[user] = getUserIncentive(user); + _incentiveIndex = _userIncentiveIndex[user] = getIncentiveIndex(); + + if (isClosed()) { + _lastUpdateTimestamp = endTimestamp; + return; + } + _lastUpdateTimestamp = block.timestamp; + + emit UpdateIncentivePool(user, _accruedIncentive[user], _incentiveIndex); + } + + /** + * @notice If user transfered lToken, accrued reward will be updated + * and user index will be set to the current index + */ + function beforeTokenTransfer(address from, address to) external override onlyLToken { + _accruedIncentive[from] = getUserIncentive(from); + _accruedIncentive[to] = getUserIncentive(to); + _userIncentiveIndex[from] = _userIncentiveIndex[to] = getIncentiveIndex(); + } + + function claimIncentive() external override { + address user = msg.sender; + + uint256 accruedIncentive = getUserIncentive(user); + + require(accruedIncentive > 0, 'NotEnoughUserAccruedIncentive'); + + _userIncentiveIndex[user] = getIncentiveIndex(); + + _accruedIncentive[user] = 0; + + IERC20(_incentiveAsset).safeTransfer(user, accruedIncentive); + + emit ClaimIncentive(user, accruedIncentive, _userIncentiveIndex[user]); + } + + function getIncentiveIndex() public view returns (uint256) { + uint256 currentTimestamp = block.timestamp < endTimestamp ? block.timestamp : endTimestamp; + uint256 timeDiff = currentTimestamp - _lastUpdateTimestamp; + uint256 totalSupply = IERC20(lToken).totalSupply(); + + if (timeDiff == 0) { + return _incentiveIndex; + } + + if (totalSupply == 0) { + return 0; + } + + uint256 IncentiveIndexDiff = (timeDiff * amountPerSecond * 1e9) / totalSupply; + + return _incentiveIndex + IncentiveIndexDiff; + } + + function getUserIncentive(address user) public view returns (uint256) { + uint256 indexDiff = 0; + + if (getIncentiveIndex() >= _userIncentiveIndex[user]) { + indexDiff = getIncentiveIndex() - _userIncentiveIndex[user]; + } + uint256 balance = IERC20(lToken).balanceOf(user); + + uint256 result = _accruedIncentive[user] + (balance * indexDiff) / 1e9; + + return result; + } + + function getUserIncentiveData(address user) + public + view + returns ( + uint256 userIndex, + uint256 userReward, + uint256 userLTokenBalance + ) + { + return (_userIncentiveIndex[user], _accruedIncentive[user], IERC20(lToken).balanceOf(user)); + } + + function getIncentivePoolData() + public + view + returns (uint256 incentiveIndex, uint256 lastUpdateTimestamp) + { + return (_incentiveIndex, _lastUpdateTimestamp); + } + + function withdrawResidue() external override onlyOwner { + require(isClosed(), 'OnlyClosed'); + uint256 residue = IERC20(_incentiveAsset).balanceOf(address(this)); + IERC20(_incentiveAsset).safeTransfer(_owner, residue); + emit IncentivePoolEnded(); + } + + /** + * @notice Admin can update amount per second + */ + function setAmountPerSecond(uint256 newAmountPerSecond) external override onlyOwner { + _incentiveIndex = getIncentiveIndex(); + + amountPerSecond = newAmountPerSecond; + _lastUpdateTimestamp = block.timestamp; + + emit RewardPerSecondUpdated(newAmountPerSecond); + } + + /** + * @notice Admin can update incentive pool end timestamp + */ + function setEndTimestamp(uint256 newEndTimestamp) external override onlyOwner { + endTimestamp = newEndTimestamp; + + emit IncentiveEndTimestampUpdated(newEndTimestamp); + } + + modifier onlyMoneyPool() { + require(msg.sender == address(_moneyPool), 'OnlyMoneyPool'); + _; + } + + modifier onlyLToken() { + require(msg.sender == address(lToken), 'OnlyLToken'); + _; + } + + modifier onlyOwner() { + require(msg.sender == _owner, 'onlyAdmin'); + _; + } +} diff --git a/contracts/interfaces/IIncentivePoolV2.sol b/contracts/interfaces/IIncentivePoolV2.sol new file mode 100644 index 0000000..9521513 --- /dev/null +++ b/contracts/interfaces/IIncentivePoolV2.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.3; + +import '../libraries/DataStruct.sol'; + +interface IIncentivePoolV2 { + event ClaimIncentive(address indexed user, uint256 claimedIncentive, uint256 userIncentiveIndex); + + event UpdateIncentivePool(address indexed user, uint256 accruedIncentive, uint256 incentiveIndex); + + event IncentivePoolEnded(); + + event RewardPerSecondUpdated(uint256 newAmountPerSecond); + + event IncentiveEndTimestampUpdated(uint256 newEndTimestamp); + + function setAmountPerSecond(uint256 newAmountPerSecond) external; + + /** + * @notice Admin can update incentive pool end timestamp + */ + function setEndTimestamp(uint256 newEndTimestamp) external; + + function updateIncentivePool(address user) external; + + function beforeTokenTransfer(address from, address to) external; + + function claimIncentive() external; + + function withdrawResidue() external; +} From d8e22cb8bc4f31d28765d28f9d79a51e09b84962 Mon Sep 17 00:00:00 2001 From: "donguk.seo" Date: Thu, 13 Jan 2022 18:25:19 +0900 Subject: [PATCH 6/6] update _lastUpdateTimestmp --- contracts/IncentivePool.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/IncentivePool.sol b/contracts/IncentivePool.sol index 2f8d89d..9cd5982 100644 --- a/contracts/IncentivePool.sol +++ b/contracts/IncentivePool.sol @@ -167,6 +167,7 @@ contract IncentivePool is IIncentivePool { _incentiveIndex = getIncentiveIndex(); amountPerSecond = newAmountPerSecond; + _lastUpdateTimestamp = block.timestamp; emit RewardPerSecondUpdated(newAmountPerSecond); }