From 029262d5e789656d040251f3af2cdfcb370dbaaa Mon Sep 17 00:00:00 2001 From: addiaddiaddi <34423634+addiaddiaddi@users.noreply.github.com> Date: Mon, 9 Oct 2023 05:13:49 -0400 Subject: [PATCH] Claim strewards and tests (#481) --- contracts/mocks/MockStProvider.sol | 4 + contracts/utils/TokenVestingStaking.sol | 54 +++++++- .../stakeStProvider/test_stProvider.py | 117 ++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) diff --git a/contracts/mocks/MockStProvider.sol b/contracts/mocks/MockStProvider.sol index 45ebe7a2..7303108b 100644 --- a/contracts/mocks/MockStProvider.sol +++ b/contracts/mocks/MockStProvider.sol @@ -96,4 +96,8 @@ contract stFLIP is ERC20 { emit Burn(msg.sender, value, refundee); _burn(msg.sender, value); } + + function mockSlash(address account, uint256 amount) public { + _burn(account, amount); + } } diff --git a/contracts/utils/TokenVestingStaking.sol b/contracts/utils/TokenVestingStaking.sol index 75fdd125..6ba8dec6 100644 --- a/contracts/utils/TokenVestingStaking.sol +++ b/contracts/utils/TokenVestingStaking.sol @@ -43,9 +43,15 @@ contract TokenVestingStaking is ITokenVestingStaking, Shared { // The contract that holds the reference addresses for staking purposes. IAddressHolder public immutable addressHolder; - + bool public revoked; + // Cumulative counter for amount staked to the st provider + uint256 public stTokenStaked; + + // Cumulative counter for amount unstaked from the st provider + uint256 public stTokenUnstaked; + /** * @param beneficiary_ address of the beneficiary to whom vested tokens are transferred * @param revoker_ the person with the power to revoke the vesting. Address(0) means it is not revocable. @@ -101,6 +107,8 @@ contract TokenVestingStaking is ITokenVestingStaking, Shared { FLIP.approve(stMinter, amount); require(IMinter(stMinter).mint(address(this), amount)); + + stTokenStaked += amount; } /** @@ -111,7 +119,51 @@ contract TokenVestingStaking is ITokenVestingStaking, Shared { (address stBurner, address stFlip) = addressHolder.getUnstakingAddresses(); IERC20(stFlip).approve(stBurner, amount); + + stTokenUnstaked += amount; + return IBurner(stBurner).burn(address(this), amount); + + } + + /** + * @notice Claims the liquid staking provider rewards. + * @param recipient_ the address to send the rewards to. If 0x0, then the beneficiary is used. + * @param amount_ the amount of rewards to claim. If greater than `totalRewards`, then all rewards are claimed. + * @dev `stTokenCounter` updates after staking/unstaking operation to keep track of the st token principle. Any amount above the + * principle is considered rewards and thus can be claimed by the beneficiary. + * + * Claim rewards flow possibilities + * 1. increment stake (staked 100, unstaked 0, balance 100) + * 2. earn rewards (staked 100, unstaked 0, balance 103) + * 3. claim rewards (staked 100, unstaked 0, balance 100) 103 + 0 - 100 = 3 + * 4. receive 3 stflip + * + * 1. stake (staked 100, unstaked 0, balance 100) + * 2. earn rewards (staked 100, unstaked 0, balance 103) + * 3. unstake all (staked 100, unstaked 103, balance 0) + * 4. claim underflows (staked 100, unstaked 103, balance 0) 0 + 103 - 100 = 3 + * 5. Need to have stflip to claim + * 1. stake (staked 100, unstaked 0, balance 100) + * 2. get slashed (staked 100, unstaked 0, balance 95) + * 3. unstake all (staked 100, unstaked 0, balance 95) + * 4. claim underflows (staked 100, unstaked 0, balance 95) 95 + 0 - 100 = -5 + * 5. must earn 5 stflip first before earning claimable rewards + * + * 1. stake (staked 100, unstaked 0, balance 100) + * 2. earn rewards (staked 100, unstaked 0, balance 103) + * 3. unstake half (staked 50, unstaked 53, balance 50) + * 4. claim rewards (staked 50, unstaked 53, balance 50) 50 + 53 - 50 = 3 + * 5. Receive 3 stflip + */ + function claimStProviderRewards(address recipient_, uint256 amount_) external onlyBeneficiary notRevoked { + (, address stFlip) = addressHolder.getUnstakingAddresses(); + uint256 totalRewards = stFLIP(stFlip).balanceOf(address(this)) + stTokenUnstaked - stTokenStaked; + + uint256 amount = amount_ > totalRewards ? totalRewards : amount_; + address recipient = recipient_ == address(0) ? beneficiary : recipient_; + + stFLIP(stFlip).transfer(recipient, amount); } /** diff --git a/tests/token_vesting/stakeStProvider/test_stProvider.py b/tests/token_vesting/stakeStProvider/test_stProvider.py index 400e4d47..4661c489 100644 --- a/tests/token_vesting/stakeStProvider/test_stProvider.py +++ b/tests/token_vesting/stakeStProvider/test_stProvider.py @@ -73,3 +73,120 @@ def test_unstakeFromStProvider(addrs, tokenVestingStaking, cf, mockStProvider): assert stFLIP.balanceOf(tv) == 0 assert cf.flip.balanceOf(staking_address) == 0 assert stFLIP.balanceOf(staking_address) == 0 + + +def test_stProviderClaimRewards(addrs, tokenVestingStaking, cf, mockStProvider): + tv, _, total = tokenVestingStaking + stFLIP, minter, _, staking_address = mockStProvider + reward_amount = 100 * 10**18 + + cf.flip.approve(minter, 2**256 - 1, {"from": addrs.DEPLOYER}) + minter.mint(addrs.DEPLOYER, reward_amount, {"from": addrs.DEPLOYER}) + + assert cf.flip.balanceOf(tv) == total + assert stFLIP.balanceOf(tv) == 0 + assert tv.stTokenStaked() == 0 + assert tv.stTokenUnstaked() == 0 + + tv.stakeToStProvider(total, {"from": addrs.BENEFICIARY}) + + assert cf.flip.balanceOf(tv) == 0 + assert stFLIP.balanceOf(tv) == total + assert tv.stTokenStaked() == total + assert tv.stTokenUnstaked() == 0 + + stFLIP.transfer(tv, reward_amount, {"from": addrs.DEPLOYER}) # earn rewards + + assert cf.flip.balanceOf(tv) == 0 + assert stFLIP.balanceOf(tv) == total + reward_amount + assert tv.stTokenStaked() == total + assert tv.stTokenUnstaked() == 0 + + tv.claimStProviderRewards( + addrs.BENEFICIARY, reward_amount, {"from": addrs.BENEFICIARY} + ) + + assert cf.flip.balanceOf(tv) == 0 + assert stFLIP.balanceOf(tv) == total + assert tv.stTokenStaked() == total + assert tv.stTokenUnstaked() == 0 + assert stFLIP.balanceOf(addrs.BENEFICIARY) == reward_amount + +def test_stProviderClaimRewardsInsufficientStflip(addrs, tokenVestingStaking, cf, mockStProvider): + tv, _, total = tokenVestingStaking + stFLIP, minter, _, staking_address = mockStProvider + reward_amount = 100 * 10**18 + + cf.flip.approve(minter, 2**256 - 1, {"from": addrs.DEPLOYER}) + minter.mint(addrs.DEPLOYER, reward_amount, {"from": addrs.DEPLOYER}) + + assert cf.flip.balanceOf(tv) == total + assert stFLIP.balanceOf(tv) == 0 + assert tv.stTokenStaked() == 0 + assert tv.stTokenUnstaked() == 0 + + tv.stakeToStProvider(total, {"from": addrs.BENEFICIARY}) + + assert cf.flip.balanceOf(tv) == 0 + assert stFLIP.balanceOf(tv) == total + assert tv.stTokenStaked() == total + assert tv.stTokenUnstaked() == 0 + + stFLIP.transfer(tv, reward_amount, {"from": addrs.DEPLOYER}) # earn rewards + + assert cf.flip.balanceOf(tv) == 0 + assert stFLIP.balanceOf(tv) == total + reward_amount + assert tv.stTokenStaked() == total + assert tv.stTokenUnstaked() == 0 + + + tv.unstakeFromStProvider(total + reward_amount, {"from": addrs.BENEFICIARY}) + + assert cf.flip.balanceOf(tv) == 0 + assert stFLIP.balanceOf(tv) == 0 + assert tv.stTokenStaked() == total + assert tv.stTokenUnstaked() == total + reward_amount + + + with reverts(REV_MSG_ERC20_EXCEED_BAL): + tv.claimStProviderRewards( + addrs.BENEFICIARY, reward_amount, {"from": addrs.BENEFICIARY} + ) + + +def test_stProviderClaimRewardsSlash(addrs, tokenVestingStaking, cf, mockStProvider): + tv, _, total = tokenVestingStaking + stFLIP, minter, _, staking_address = mockStProvider + slash_amount = 100 * 10**18 + + assert cf.flip.balanceOf(tv) == total + assert stFLIP.balanceOf(tv) == 0 + assert tv.stTokenStaked() == 0 + assert tv.stTokenUnstaked() == 0 + + tv.stakeToStProvider(total, {"from": addrs.BENEFICIARY}) + + assert cf.flip.balanceOf(tv) == 0 + assert stFLIP.balanceOf(tv) == total + assert tv.stTokenStaked() == total + assert tv.stTokenUnstaked() == 0 + + stFLIP.mockSlash(tv, slash_amount, {"from": addrs.DEPLOYER}) + + assert cf.flip.balanceOf(tv) == 0 + assert stFLIP.balanceOf(tv) == total - slash_amount + assert tv.stTokenStaked() == total + assert tv.stTokenUnstaked() == 0 + + tv.unstakeFromStProvider(total - slash_amount, {"from": addrs.BENEFICIARY}) + + assert cf.flip.balanceOf(tv) == 0 + assert stFLIP.balanceOf(tv) == 0 + assert tv.stTokenStaked() == total + assert tv.stTokenUnstaked() == total - slash_amount + + + with reverts(REV_MSG_INTEGER_OVERFLOW): + tv.claimStProviderRewards( + addrs.BENEFICIARY, 2**256 - 1, {"from": addrs.BENEFICIARY} + ) \ No newline at end of file