Skip to content

Commit

Permalink
Feat/claim st rewards (#482)
Browse files Browse the repository at this point in the history
* Claim strewards and tests (#481)

* chore: remove beneficiary and minor improvements

* test: add modifier tests

* chore: reformat comment

* chore: reorder stTokenStaked increase

---------

Co-authored-by: addiaddiaddi <34423634+addiaddiaddi@users.noreply.github.com>
  • Loading branch information
albert-llimos and addiaddiaddi authored Oct 11, 2023
1 parent 45b297d commit 8466176
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 5 deletions.
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})

0 comments on commit 8466176

Please sign in to comment.