Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/claim st rewards #482

Merged
merged 6 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions contracts/interfaces/IAddressHolder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,7 @@ interface IAddressHolder {

function getUnstakingAddresses() external view returns (address, address);

function getStFlip() external view returns (address);

function getGovernor() external view returns (address);
}
2 changes: 2 additions & 0 deletions contracts/interfaces/ITokenVestingStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ interface ITokenVestingStaking {

function unstakeFromStProvider(uint256 amount) external returns (uint256);

function claimStProviderRewards(uint256 amount_) external;

function release(IERC20 token) external;

function revoke(IERC20 token) external;
Expand Down
4 changes: 4 additions & 0 deletions contracts/mocks/MockStProvider.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
4 changes: 4 additions & 0 deletions contracts/utils/AddressHolder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ contract AddressHolder is IAddressHolder, Shared {
return (stBurner, stFLIP);
}

function getStFlip() external view override returns (address) {
return stFLIP;
}

/// @dev Getter function for the governor address
function getGovernor() external view override returns (address) {
return governor;
Expand Down
33 changes: 30 additions & 3 deletions contracts/utils/TokenVestingStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ contract TokenVestingStaking is ITokenVestingStaking, Shared {

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.
Expand Down Expand Up @@ -100,6 +106,9 @@ contract TokenVestingStaking is ITokenVestingStaking, Shared {
address stMinter = addressHolder.getStakingAddress();

FLIP.approve(stMinter, amount);

stTokenStaked += amount;

require(IMinter(stMinter).mint(address(this), amount));
}

Expand All @@ -111,9 +120,27 @@ 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 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.
*/
function claimStProviderRewards(uint256 amount_) external override onlyBeneficiary notRevoked {
address stFlip = addressHolder.getStFlip();
uint256 totalRewards = stFLIP(stFlip).balanceOf(address(this)) + stTokenUnstaked - stTokenStaked;

uint256 amount = amount_ > totalRewards ? totalRewards : amount_;

stFLIP(stFlip).transfer(beneficiary, amount);
}

/**
* @notice Transfers vested tokens to beneficiary.
* @param token ERC20 token which is being vested.
Expand All @@ -128,9 +155,9 @@ contract TokenVestingStaking is ITokenVestingStaking, Shared {
}

/**
* @notice Allows the revoker to revoke the vesting and stop the beneficiary from releasing
* any tokens if the vesting period has not bene completed. Any staked tokens at the time of
* revoking can be retrieved by the revoker upon unstaking via `retrieveRevokedFunds`.
* @notice Allows the revoker to revoke the vesting and stop the beneficiary from releasing any
* tokens if the vesting period has not bene completed. Any staked tokens at the time of
* revoking can be retrieved by the revoker upon unstaking via `retrieveRevokedFunds`.
* @param token ERC20 token which is being vested.
*/
function revoke(IERC20 token) external override onlyRevoker notRevoked {
Expand Down
151 changes: 149 additions & 2 deletions tests/token_vesting/stakeStProvider/test_stProvider.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ def test_stakeToStProvider(addrs, tokenVestingStaking, cf, mockStProvider):
assert stFLIP.balanceOf(staking_address) == 0


def test_stake_unstake_rev_sender(addrs, tokenVestingStaking):
def test_rev_sender(addrs, tokenVestingStaking):
tv, _, total = tokenVestingStaking
with reverts(REV_MSG_NOT_BENEFICIARY):
tv.stakeToStProvider(total, {"from": addrs.DEPLOYER})
with reverts(REV_MSG_NOT_BENEFICIARY):
tv.unstakeFromStProvider(0, {"from": addrs.DEPLOYER})
with reverts(REV_MSG_NOT_BENEFICIARY):
tv.claimStProviderRewards(0, {"from": addrs.DEPLOYER})


def test_stake_unstake_rev_amount(addrs, tokenVestingStaking):
def test_rev_amount(addrs, tokenVestingStaking):
tv, _, total = tokenVestingStaking
with reverts(REV_MSG_ERC20_EXCEED_BAL):
tv.stakeToStProvider(total + 1, {"from": addrs.BENEFICIARY})
Expand All @@ -53,6 +55,16 @@ def test_unstake_rev_revoked(addrs, tokenVestingStaking, mockStProvider):
tv.unstakeFromStProvider(1, {"from": addrs.BENEFICIARY})


def test_claim_rev_revoked(addrs, tokenVestingStaking, mockStProvider):
tv, _, _ = tokenVestingStaking
stFLIP, _, _, _ = mockStProvider

tv.revoke(stFLIP, {"from": addrs.REVOKER})

with reverts(REV_MSG_TOKEN_REVOKED):
tv.claimStProviderRewards(1, {"from": addrs.BENEFICIARY})


def test_unstakeFromStProvider(addrs, tokenVestingStaking, cf, mockStProvider):
tv, _, total = tokenVestingStaking
stFLIP, _, burner, staking_address = mockStProvider
Expand All @@ -73,3 +85,138 @@ 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


## 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


def test_stProviderClaimRewards(addrs, tokenVestingStaking, cf, mockStProvider):
tv, _, total = tokenVestingStaking
stFLIP, minter, _, _ = 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(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, _, _ = 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(reward_amount, {"from": addrs.BENEFICIARY})


def test_stProviderClaimRewardsSlash(addrs, tokenVestingStaking, cf, mockStProvider):
tv, _, total = tokenVestingStaking
stFLIP, _, _, _ = 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(2**256 - 1, {"from": addrs.BENEFICIARY})