From f35d8f10fe3f08e9072043e71d4a3b604c2593fd Mon Sep 17 00:00:00 2001 From: BkChoy Date: Thu, 22 Feb 2024 20:49:12 +1300 Subject: [PATCH] insurance pool fixes --- contracts/core/InsurancePool.sol | 60 ++++++++++++++++++- contracts/core/RewardsPoolTimeBased.sol | 2 +- contracts/core/base/StakingRewardsPool.sol | 8 +++ contracts/core/tokens/base/ERC677.sol | 4 +- .../core/tokens/base/ERC677Upgradeable.sol | 4 +- test/core/insurance-pool.test.ts | 14 +++++ test/core/priorityPool/priority-pool.test.ts | 5 ++ test/core/rebase-controller.test.ts | 2 + test/core/staking-pool.test.ts | 1 + test/core/wrapped-sd-token.test.ts | 1 + test/linkStaking/operator-vcs.test.ts | 2 + .../liquid-sd-index-pool.test.ts | 2 +- 12 files changed, 100 insertions(+), 5 deletions(-) diff --git a/contracts/core/InsurancePool.sol b/contracts/core/InsurancePool.sol index 4bfd89bd..707beca1 100644 --- a/contracts/core/InsurancePool.sol +++ b/contracts/core/InsurancePool.sol @@ -20,27 +20,38 @@ contract InsurancePool is StakingRewardsPool { uint256 public maxClaimAmountBP; bool public claimInProgress; + uint64 public withdrawalDelayDuration; + uint64 public withdrawalWindowDuration; + mapping(address => uint64) private withdrawalRequests; + event InitiateClaim(); event ExecuteClaim(uint256 amount); event ResolveClaim(); + event RequestWithdrawal(address indexed account, uint64 withdrawalStartTime); + event SetWithdrawalParams(uint64 withdrawalDelayDuration, uint64 withdrawalWindowDuration); error SenderNotAuthorized(); error ClaimInProgress(); error ExceedsMaxClaimAmount(); error InvalidClaimAmount(); error NoClaimInProgress(); + error WithdrawalWindowInactive(); function initialize( address _lpToken, string memory _liquidTokenName, string memory _liquidTokenSymbol, address _rebaseController, - uint256 _maxClaimAmountBP + uint256 _maxClaimAmountBP, + uint64 _withdrawalDelayDuration, + uint64 _withdrawalWindowDuration ) public initializer { __StakingRewardsPool_init(_lpToken, _liquidTokenName, _liquidTokenSymbol); rebaseController = _rebaseController; if (_maxClaimAmountBP > 9000) revert InvalidClaimAmount(); maxClaimAmountBP = _maxClaimAmountBP; + withdrawalDelayDuration = _withdrawalDelayDuration; + withdrawalWindowDuration = _withdrawalWindowDuration; } modifier onlyRebaseController() { @@ -55,9 +66,12 @@ contract InsurancePool is StakingRewardsPool { /** * @notice deposits tokens into the pool + * @dev will delete any active or upcoming withdrawal window * @param _amount amount of tokens to deposit */ function deposit(uint256 _amount) external whileNoClaimInProgress { + if (withdrawalRequests[msg.sender] != 0) delete withdrawalRequests[msg.sender]; + rewardsPool.updateReward(msg.sender); token.safeTransferFrom(msg.sender, address(this), _amount); _mint(msg.sender, _amount); @@ -69,12 +83,45 @@ contract InsurancePool is StakingRewardsPool { * @param _amount amount of tokens to withdraw */ function withdraw(uint256 _amount) external whileNoClaimInProgress { + if (!canWithdraw(msg.sender)) revert WithdrawalWindowInactive(); + rewardsPool.updateReward(msg.sender); _burn(msg.sender, _amount); totalDeposits -= _amount; token.safeTransfer(msg.sender, _amount); } + /** + * @notice requests a withdrawal and initiates the withdrawal delay period + */ + function requestWithdrawal() external { + uint64 withdrawalStartTime = uint64(block.timestamp) + withdrawalDelayDuration; + withdrawalRequests[msg.sender] = withdrawalStartTime; + emit RequestWithdrawal(msg.sender, withdrawalStartTime); + } + + /** + * @notice returns whether an account's withdrawal is active + * @param _account address of account + * @return canWithdraw whether withdrawal window is active + */ + function canWithdraw(address _account) public view returns (bool) { + if (withdrawalDelayDuration == 0) return true; + (uint64 start, uint64 end) = getWithdrawalWindow(_account); + return block.timestamp >= start && block.timestamp < end; + } + + /** + * @notice returns an account's current active or upcoming withdrawal window + * @param _account address of account + * @return start time and end time of withdrawal window + */ + function getWithdrawalWindow(address _account) public view returns (uint64, uint64) { + uint64 withdrawalStartTime = withdrawalRequests[_account]; + if (withdrawalDelayDuration == 0 || block.timestamp >= withdrawalStartTime + withdrawalWindowDuration) return (0, 0); + return (withdrawalStartTime, withdrawalStartTime + withdrawalWindowDuration); + } + /** * @notice initiates the claim process */ @@ -154,6 +201,17 @@ contract InsurancePool is StakingRewardsPool { maxClaimAmountBP = _maxClaimAmountBP; } + /** + * @notice sets the withdrawal parameters + * @param _withdrawalDelayDuration amount of time required to wait before withdrawaing + * @param _withdrawalWindowDuration amount of time a withdrawal can be executed for after the delay has elapsed + */ + function setWithdrawalParams(uint64 _withdrawalDelayDuration, uint64 _withdrawalWindowDuration) external onlyOwner { + withdrawalDelayDuration = _withdrawalDelayDuration; + withdrawalWindowDuration = _withdrawalWindowDuration; + emit SetWithdrawalParams(_withdrawalDelayDuration, _withdrawalWindowDuration); + } + /** * @notice returns the total amount of assets staked in the pool * @return total staked amount diff --git a/contracts/core/RewardsPoolTimeBased.sol b/contracts/core/RewardsPoolTimeBased.sol index 976ae0b6..e21b8422 100644 --- a/contracts/core/RewardsPoolTimeBased.sol +++ b/contracts/core/RewardsPoolTimeBased.sol @@ -70,7 +70,7 @@ contract RewardsPoolTimeBased is RewardsPool, Ownable { uint256 remainingRewards = timeOfLastRewardUpdate >= epochExpiry ? 0 - : (epochExpiry - timeOfLastRewardUpdate) * getLastRewardPerSecond(); + : ((epochExpiry - timeOfLastRewardUpdate) * epochRewardsAmount) / epochDuration; totalRewards += _rewardsAmount; epochRewardsAmount = remainingRewards + _rewardsAmount; diff --git a/contracts/core/base/StakingRewardsPool.sol b/contracts/core/base/StakingRewardsPool.sol index 1a6c499a..edec41e3 100644 --- a/contracts/core/base/StakingRewardsPool.sol +++ b/contracts/core/base/StakingRewardsPool.sol @@ -12,6 +12,8 @@ import "../tokens/base/ERC677Upgradeable.sol"; * @dev Rewards can be positive or negative (user balances can increase and decrease) */ abstract contract StakingRewardsPool is ERC677Upgradeable, UUPSUpgradeable, OwnableUpgradeable { + uint256 private constant DEAD_SHARES = 10**3; + IERC20Upgradeable public token; mapping(address => uint256) private shares; @@ -184,6 +186,12 @@ abstract contract StakingRewardsPool is ERC677Upgradeable, UUPSUpgradeable, Owna function _mintShares(address _recipient, uint256 _amount) internal { require(_recipient != address(0), "Mint to the zero address"); + if (totalShares == 0) { + shares[address(0)] = DEAD_SHARES; + totalShares = DEAD_SHARES; + _amount -= DEAD_SHARES; + } + totalShares += _amount; shares[_recipient] += _amount; } diff --git a/contracts/core/tokens/base/ERC677.sol b/contracts/core/tokens/base/ERC677.sol index 60053c07..63c4f437 100644 --- a/contracts/core/tokens/base/ERC677.sol +++ b/contracts/core/tokens/base/ERC677.sol @@ -11,7 +11,9 @@ contract ERC677 is ERC20 { string memory _tokenSymbol, uint256 _totalSupply ) ERC20(_tokenName, _tokenSymbol) { - _mint(msg.sender, _totalSupply * (10**uint256(decimals()))); + if (_totalSupply != 0) { + _mint(msg.sender, _totalSupply * (10**uint256(decimals()))); + } } function transferAndCall( diff --git a/contracts/core/tokens/base/ERC677Upgradeable.sol b/contracts/core/tokens/base/ERC677Upgradeable.sol index 56eaade4..2c4cc576 100644 --- a/contracts/core/tokens/base/ERC677Upgradeable.sol +++ b/contracts/core/tokens/base/ERC677Upgradeable.sol @@ -12,7 +12,9 @@ contract ERC677Upgradeable is ERC20Upgradeable { uint256 _totalSupply ) public onlyInitializing { __ERC20_init(_tokenName, _tokenSymbol); - _mint(msg.sender, _totalSupply * (10**uint256(decimals()))); + if (_totalSupply != 0) { + _mint(msg.sender, _totalSupply * (10**uint256(decimals()))); + } } function transferAndCall( diff --git a/test/core/insurance-pool.test.ts b/test/core/insurance-pool.test.ts index 7b28bd44..0321d642 100644 --- a/test/core/insurance-pool.test.ts +++ b/test/core/insurance-pool.test.ts @@ -44,6 +44,8 @@ describe('InsurancePool', () => { 'symbol', accounts[0], 3000, + 0, + 0, ])) as InsurancePool rewardsPool = (await deploy('RewardsPoolTimeBased', [ @@ -59,11 +61,13 @@ describe('InsurancePool', () => { .connect(signers[1]) .approve(insurancePool.address, ethers.constants.MaxUint256) await token.approve(rewardsPool.address, ethers.constants.MaxUint256) + await insurancePool.deposit(1000) }) it('deposit should work correctly', async () => { await insurancePool.deposit(toEther(1000)) await insurancePool.connect(signers[1]).deposit(toEther(3000)) + await insurancePool.withdraw(1000) assert.equal(fromEther(await insurancePool.balanceOf(accounts[0])), 1000) assert.equal(fromEther(await insurancePool.balanceOf(accounts[1])), 3000) @@ -87,8 +91,16 @@ describe('InsurancePool', () => { }) it('withdraw should work correctly', async () => { + await insurancePool.setWithdrawalParams(10, 100) await insurancePool.deposit(toEther(1200)) await insurancePool.connect(signers[1]).deposit(toEther(3000)) + + await expect(insurancePool.withdraw(toEther(200))).to.be.revertedWith( + 'WithdrawalWindowInactive()' + ) + + await insurancePool.requestWithdrawal() + await time.increase(10) await insurancePool.withdraw(toEther(200)) assert.equal(fromEther(await insurancePool.balanceOf(accounts[0])), 1000) @@ -105,6 +117,8 @@ describe('InsurancePool', () => { assert.equal(fromEther(await rewardsPool.userRewards(accounts[0])), 0) assert.equal(fromEther(await rewardsPool.userRewards(accounts[1])), 0) + await insurancePool.connect(signers[1]).requestWithdrawal() + await time.increase(10) await insurancePool.connect(signers[1]).withdraw(toEther(100)) assert.equal(fromEther(await rewardsPool.rewardPerToken()), 0.25) diff --git a/test/core/priorityPool/priority-pool.test.ts b/test/core/priorityPool/priority-pool.test.ts index b6d8e325..3d8039c4 100644 --- a/test/core/priorityPool/priority-pool.test.ts +++ b/test/core/priorityPool/priority-pool.test.ts @@ -71,6 +71,8 @@ describe('PriorityPool', () => { for (let i = 0; i < signers.length; i++) { await token.connect(signers[i]).approve(sq.address, ethers.constants.MaxUint256) } + + await sq.deposit(1000, false) }) it('deposit should work correctly', async () => { @@ -136,6 +138,7 @@ describe('PriorityPool', () => { it('depositQueuedTokens should work correctly', async () => { await sq.deposit(toEther(2000), true) + await sq.withdraw(1000, 0, 0, [], true) await token.transfer(strategy.address, toEther(1000)) await stakingPool.updateStrategyRewards([0], '0x') await sq.connect(signers[1]).deposit(toEther(500), true) @@ -235,6 +238,7 @@ describe('PriorityPool', () => { it('performUpkeep should work corectly', async () => { await sq.deposit(toEther(2000), true) + await sq.withdraw(1000, 0, 0, [], true) await token.transfer(strategy.address, toEther(1000)) await stakingPool.updateStrategyRewards([0], '0x') await sq.connect(signers[1]).deposit(toEther(500), true) @@ -594,6 +598,7 @@ describe('PriorityPool', () => { await stakingPool.connect(signers[1]).approve(sq.address, ethers.constants.MaxUint256) await stakingPool.connect(signers[2]).approve(sq.address, ethers.constants.MaxUint256) await sq.deposit(toEther(1000), true) + await sq.withdraw(1000, 0, 0, [], true) await token.transfer(strategy.address, toEther(1000)) await stakingPool.updateStrategyRewards([0], '0x') await sq.connect(signers[1]).deposit(toEther(100), true) diff --git a/test/core/rebase-controller.test.ts b/test/core/rebase-controller.test.ts index a78a87bf..29fc6ece 100644 --- a/test/core/rebase-controller.test.ts +++ b/test/core/rebase-controller.test.ts @@ -73,6 +73,8 @@ describe('RebaseController', () => { 'symbol', accounts[0], 3000, + 10, + 100, ])) as InsurancePool rebaseController = (await deploy('RebaseController', [ diff --git a/test/core/staking-pool.test.ts b/test/core/staking-pool.test.ts index 4f4ad5db..eb60572a 100644 --- a/test/core/staking-pool.test.ts +++ b/test/core/staking-pool.test.ts @@ -83,6 +83,7 @@ describe('StakingPool', () => { await stakingPool.setRebaseController(accounts[0]) await token.approve(stakingPool.address, ethers.constants.MaxUint256) + await stakingPool.deposit(accounts[0], 1000) }) it('derivative token metadata should be correct', async () => { diff --git a/test/core/wrapped-sd-token.test.ts b/test/core/wrapped-sd-token.test.ts index c660dd1e..72f128fb 100644 --- a/test/core/wrapped-sd-token.test.ts +++ b/test/core/wrapped-sd-token.test.ts @@ -63,6 +63,7 @@ describe('WrappedSDToken', () => { await stakingPool.setRebaseController(accounts[0]) await token.approve(stakingPool.address, ethers.constants.MaxUint256) + await stakingPool.deposit(accounts[0], 1000) }) it('token metadata should be correct', async () => { diff --git a/test/linkStaking/operator-vcs.test.ts b/test/linkStaking/operator-vcs.test.ts index d44859e5..5cd3184f 100644 --- a/test/linkStaking/operator-vcs.test.ts +++ b/test/linkStaking/operator-vcs.test.ts @@ -85,6 +85,7 @@ describe('OperatorVCS', () => { await token.approve(stakingPool.address, ethers.constants.MaxUint256) await token.transfer(rewardsController.address, toEther(10000)) await token.transfer(pfAlertsController.address, toEther(10000)) + await stakingPool.deposit(accounts[0], 1000) }) it('should be able to add vault', async () => { @@ -210,6 +211,7 @@ describe('OperatorVCS', () => { it('updateDeposits should work correctly with reward withdrawals', async () => { await stakingPool.deposit(accounts[0], toEther(1000)) + await stakingPool.withdraw(accounts[0], accounts[0], 1000) await rewardsController.setReward(vaults[1], toEther(5)) await rewardsController.setReward(vaults[3], toEther(7)) await rewardsController.setReward(vaults[5], toEther(8)) diff --git a/test/liquidSDIndex/liquid-sd-index-pool.test.ts b/test/liquidSDIndex/liquid-sd-index-pool.test.ts index f9e233ea..47f6358c 100644 --- a/test/liquidSDIndex/liquid-sd-index-pool.test.ts +++ b/test/liquidSDIndex/liquid-sd-index-pool.test.ts @@ -127,7 +127,7 @@ describe('LiquidSDIndexPool', () => { 'Composition targets must sum to 100%' ) - await pool.connect(signers[1]).withdraw(toEther(3000)) + await pool.connect(signers[1]).withdraw(toEther(2999)) await pool.removeLSDToken(lsd2.address, [2000, 8000]) assert.deepEqual(await pool.getLSDTokens(), [lsd1.address, lsd3.address])