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

Cantina contest fixes #25

Merged
merged 27 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3283861
fix: Cantina 154
kasperpawlowski Jul 2, 2024
b55e0dc
test: update tests due to Cantina 154 fix
kasperpawlowski Jul 2, 2024
b364c44
fix: cantina 167
kasperpawlowski Jul 2, 2024
967f652
fix: cantina 231
kasperpawlowski Jul 2, 2024
8fd3d52
test: add test for cantina 231 fix
kasperpawlowski Jul 2, 2024
7db4407
fix: docs consistency
kasperpawlowski Jul 2, 2024
929b626
fix: formatting
kasperpawlowski Jul 2, 2024
be7a1c9
fix: cantina 240
kasperpawlowski Jul 2, 2024
e2248ec
fix: cantina 273
kasperpawlowski Jul 2, 2024
9cfe713
fix: cantina 313
kasperpawlowski Jul 2, 2024
47f56d3
fix: cantina 339
kasperpawlowski Jul 2, 2024
6165fd6
fix: cantina 233
kasperpawlowski Jul 2, 2024
bcab14e
Merge pull request #1 from euler-xyz/cantina-154
kasperpawlowski Jul 2, 2024
b84078a
Merge pull request #2 from euler-xyz/cantina-167
kasperpawlowski Jul 2, 2024
dd9b9fc
Merge pull request #3 from euler-xyz/cantina-231
kasperpawlowski Jul 2, 2024
0987950
fix: typo
kasperpawlowski Jul 2, 2024
bd3501a
Merge pull request #4 from euler-xyz/cantina-240
kasperpawlowski Jul 2, 2024
9552296
Merge branch 'master' into cantina-273
kasperpawlowski Jul 2, 2024
9a21f9d
Merge pull request #5 from euler-xyz/cantina-273
kasperpawlowski Jul 2, 2024
bd16176
Merge pull request #6 from euler-xyz/cantina-313
kasperpawlowski Jul 2, 2024
fd26ab1
Merge pull request #7 from euler-xyz/cantina-339
kasperpawlowski Jul 2, 2024
963293d
Merge pull request #8 from euler-xyz/cantina-233
kasperpawlowski Jul 2, 2024
32c2c2f
fix: cantina 31
kasperpawlowski Jul 5, 2024
4bfa8a1
Merge pull request #9 from euler-xyz/cantina-31
kasperpawlowski Jul 6, 2024
920605c
refactor: DistributionStorage refactor
kasperpawlowski Jul 13, 2024
d124216
Merge pull request #10 from euler-xyz/refactor
kasperpawlowski Jul 15, 2024
1e0b4b9
Merge branch 'master' into cantina-fixes
hoytech Aug 13, 2024
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
26 changes: 14 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,13 @@ Reward Streams operates in two modes of rewards distribution: staking and balanc

The balance-tracking `TrackingRewardStreams` implementation inherits from the `BaseRewardStreams` contract. It defines the `IBalanceTracker.balanceTrackerHook` function, which is required to be called on every transfer of the rewarded token if a user opted in for the hook to be called.

In this mode, the rewarded token contract not only calls the `balanceTrackerHook` function whenever a given account balance changes, but also implements the `IBalanceForwarder` interface. This interface defines two functions: `enableBalanceForwarding` and `disableBalanceForwarding`, which are used to opt in and out of the hook being called.

### Staking Reward Distribution

The staking `StakingRewardStreams` implementation also inherits from the `BaseRewardStreams` contract. It defines two functions: `stake` and `unstake`, which are used to stake and unstake the rewarded token.

In both modes, each distributor contract defines an `EPOCH_DURATION` constant, which is the duration of a single epoch. This duration cannot be less than 1 week and more than 10 weeks.

When registering a new reward stream for the `rewarded` token, one needs to specify the `startEpoch` number when the new stream will come into effect. `startEpoch` cannot be more than 5 epochs into the future. Moreover, one needs to specify `rewardAmounts` array which instructs the contract how much `reward` one wants to distribute in each epoch starting from `startEpoch`. The `rewardAmounts` array must have a length of at most 25.
When registering a new reward stream for the `rewarded` token, one must specify the `startEpoch` number when the new stream will come into effect. To protect users from obvious mistakes, the distributor contract enforces a soft requirement that ensures the `startEpoch` is not more than 5 epochs into the future. Moreover, one must specify the `rewardAmounts` array, which instructs the contract how much `reward` one wants to distribute in each epoch starting from `startEpoch`. The `rewardAmounts` array must have a length of at most 25 for one function call.

If rewarded epochs of multiple reward streams overlap, the amounts will be combined and the effective distribution will be the sum of the amounts in the overlapping epochs.

Expand Down Expand Up @@ -97,17 +95,17 @@ Given this, Alice decides to enable only the `GHI` reward and keep the `DEF` rew

---

Multiple functions of the distributors contain an additional boolean parameter called `forfeitRecentReward`. It allows a user to optimize gas consumption in case it is not worth to iterate over multiple distribution epochs and updating contract storage. It also allows for "emergency exit" for operations like disabling reward and claiming, and DOS protection (i.e. in liquidation flows).
Multiple functions of the distributors contain an additional boolean parameter called `forfeitRecentReward`/`ignoreRecentReward`. It allows a user to optimize gas consumption in case it is not worth to iterate over multiple distribution epochs and updating contract storage. It also allows for "emergency exit" for operations like disabling reward and claiming, and DOS protection (i.e. in liquidation flows).

As previously explained, rewards distributions are epoch-based. Thanks to that, each epoch may have a different reward rate, but also it is possible for the reward streams to be registered permissionlessly in additive manner. However, the downside of this approach is the fact that whenever a user stakes or unstakes (or, for balance-tracking version of the distributor, transfers/mints/burns the rewarded token), the distributor contract needs to iterate over all the epochs since the last time given distribution, defined by `rewarded` and `reward` token, was updated. Moreover, a user may be earning multiple rewards for a single rewarded token, so the distributor contract needs to iterate over all the epochs since the last update for all the rewards the user is earning. If updates happen rarely (i.e. due to low staking/unstaking activity of the `rewarded` token for a given `reward`), the gas cost associated with iterating may be significant, affecting user's profitability. Hence, when disabling or claiming reward, if the user wants to skip the epochs iteration, they can call the relevant function with `forfeitRecentReward` set to `true`. This will grant the rewards earned since the last distribution update, which would normally be earned by the user, to the rest of the distribution participants, lowering the gas cost for the user.
As previously explained, rewards distributions are epoch-based. Thanks to that, each epoch may have a different reward rate, but also it is possible for the reward streams to be registered permissionlessly in additive manner. However, the downside of this approach is the fact that whenever a user stakes or unstakes (or, for balance-tracking version of the distributor, transfers/mints/burns the rewarded token), the distributor contract needs to iterate over all the epochs since the last time given distribution, defined by `rewarded` and `reward` token, was updated. Moreover, a user may be earning multiple rewards for a single rewarded token, so the distributor contract needs to iterate over all the epochs since the last update for all the rewards the user is earning. If updates happen rarely (i.e. due to low staking/unstaking activity of the `rewarded` token for a given `reward`), the gas cost associated with iterating may be significant, affecting user's profitability. Hence, i.e. when disabling or claiming reward, if the user wants to skip the epochs iteration, they can call the relevant function with `forfeitRecentReward`/`ignoreRecentReward` set to `true`. This will, depending on the operation, either grant the rewards earned since the last distribution update, which would normally be earned by the user, to the rest of the distribution participants or ignore them by skiping the iteration, lowering the gas cost for the user.

`forfeitRecentReward` parameter may also come handy for the rewarded token contract which calls `balanceTrackerHook` on the balance changes. In case of i.e. liquidation, where user may have incentive to perform DOS attack and increase gas cost of the token transfer by enabling multiple rewards for distributions of low activity, the rewarded token contract may call `balanceTrackerHook` with `forfeitRecentReward` set to `true` to lower the gas cost of the transfer. Unfortunately, this may lead to the user losing part of their rewards.

---

### Example:

Alice staked her `ABC` and decided to enable both `DEF` and `GHI` rewards. Alice now wants to unstake her `ABC`, but notices that despite her estimations `GHI` tokens that she earned have very low value. It's been some time since the `GHI` distribution was updated last time hence the gas cost associated with unstaking may be significant. Alice may decide to either call `unstake` with `forgiveRecentReward` set to `true`, which means that both `DEF` and `GHI` rewards that she would earn since the last updates would get lost in favor of the rest of participants. Or she may first call `disableReward(GHI)` with `forgiveRecentReward` set to `true`, which will skip epochs iteration for `GHI` distribution, and then call `unstake` with `forgiveRecentReward` set to `false`, keeping all the `DEF` rewards.
Alice staked her `ABC` and decided to enable both `DEF` and `GHI` rewards. Alice now wants to unstake her `ABC`, but notices that despite her estimations `GHI` tokens that she earned have very low value. It's been some time since the `GHI` distribution was updated last time hence the gas cost associated with unstaking may be significant. Alice may decide to either call `unstake` with `forfeitRecentReward` set to `true`, which means that both `DEF` and `GHI` rewards that she would earn since the last updates would get lost in favor of the rest of participants. Or she may first call `disableReward(GHI)` with `forfeitRecentReward` set to `true`, which will skip epochs iteration for `GHI` distribution, and then call `unstake` with `forfeitRecentReward` set to `false`, keeping all the `DEF` rewards.

---

Expand All @@ -116,14 +114,14 @@ Unlike other permissioned distributors based on the billion-dollar algorithm, Re
## Known limitations

1. **Epoch duration may not be shorter than 1 week and longer than 10 weeks**: This limitation is in place to ensure the stability and efficiency of the distribution system. The longer the epoch, the more gas efficient the distribution is.
2. **New reward stream may start at most 5 epochs ahead and be at most 25 epochs long**: This limitation is in place not to register distribution too far in the future and lasting for too long.
2. **Registered reward stream can start at most 5 epochs ahead and can last for a maximum of 25 epochs**: This limitation ensures that user inputs are reasonable and helps protect them from making obvious mistakes.
3. **A user may have at most 5 rewards enabled at a time for a given rewarded token**: This limitation is in place to prevent users from enabling an excessive number of rewards, which could lead to increased gas costs and potential system instability.
4. **During its lifetime, a distributor may distribute at most `type(uint160).max / 2e19` units of a reward token per rewarded token**: This limitation is in place not to allow accumulator overflow.
5. **Not all rewarded-reward token pairs may be compatible with the distributor**: This limitation may occur due to unfortunate rounding errors during internal calculations, which can result in registered rewards being irrevocably lost. To avoid this, one must ensure that the following condition, based on an empirical formula, holds true:

`6e6 * block_time_sec * expected_apr_percent * 10**reward_token_decimals * price_of_rewarded_token / 10**rewarded_token_decimals / price_of_reward_token > 1`

For example, for the SHIB-USDC reward-rewarded pair, the above condition will not be met, even with an unusually high assumed APR of 1000%:
For example, for the SHIB-USDC rewarded-reward pair, the above condition will not be met, even with an unusually high assumed APR of 1000%:
`block_time_sec = 12`
`expected_apr_percent = 1000`
`rewarded_token_decimals = 18`
Expand All @@ -134,10 +132,14 @@ For example, for the SHIB-USDC reward-rewarded pair, the above condition will no
`6e6 * 12 * 1000 * 10**6 * 0.00002 / 10**18 / 1` is less than `1`.

6. **If nobody earns rewards at the moment (i.e. nobody staked/deposited yet), they're being virtually accrued by address(0) and may be claimed by anyone**: This feature is designed to prevent reward tokens from being lost when nobody earns them at the moment. However, it also means that unclaimed rewards could potentially be claimed by anyone.
7. **Distributor contracts do not have an owner or admin meaning that none of the assets can be directly recovered from them**: This feature is required for the system to work in a permissionless manner. However, it also means that if a mistake is made in the distribution of rewards, the assets cannot be directly recovered from the distributor contracts.
8. **Distributor contracts do not support rebasing and fee-on-transfer tokens**: This limitation is in place due to internal accounting system limitations. Neither reward nor rewarded tokens may be rebasing or fee-on-transfer tokens.
9. **Precision loss may lead to the portion of rewards being lost to the distributor**: Precision loss is inherent to calculations in Solidity due to its use of fixed-point arithmetic. In some configurations of the distributor and streams, depending on the accumulator update frequency, a dust portion of the rewards registered might get irrevocably lost to the distributor. However, the amount lost should be negligible as long as the condition from 5. is met.
10. **Permissionless nature of the distributor may lead to DOS for popular reward-rewarded token pairs**: The distributor allows anyone to incentivize any token with any reward. A bad actor may grief the party willing to legitimately incentivize a given reward-rewarded token pair by registering a tiny reward stream long before the party decides to register the reward stream themselves. Such a reward stream, if not updated frequently, may lead to a situation where the legitimate party is forced to update it themselves since the time the bad actor set up their stream. This may be costly in terms of gas.
7. **If nobody earns rewards at the moment, despite being virtually accrued by address(0) and claimable by anyone, they might still get lost due to rounding**: This limitation may occur due to unfortunate rounding errors during internal calculations, which can result in registered rewards being irrevocably lost. To ensure that the value lost due to rounding is not significant, one must ensure that 1 wei of the reward token multiplied by epoch duration has negligible value.

For example, if the epoch duration is 2 weeks (which corresponds to ~1.2e6 seconds) and the reward token is WBTC, in one rounding, up to ~1.2e6 WBTC may be lost. At the current BTC price, this value corresponds to ~$700, which is a significant value to lose for just one update of the reward stream. Hence, one must either avoid adding rewards that have a significant value of 1 wei or make sure that someone earns rewards at all times.

8. **Distributor contracts do not have an owner or admin meaning that none of the assets can be directly recovered from them**: This feature is required for the system to work in a permissionless manner. However, it also means that if a mistake is made in the distribution of rewards, the assets cannot be directly recovered from the distributor contracts.
9. **Distributor contracts do not support rebasing and fee-on-transfer tokens**: This limitation is in place due to internal accounting system limitations. Neither reward nor rewarded tokens may be rebasing or fee-on-transfer tokens.
10. **Precision loss may lead to the portion of rewards being lost to the distributor**: Precision loss is inherent to calculations in Solidity due to its use of fixed-point arithmetic. In some configurations of the distributor and streams, depending on the accumulator update frequency, a dust portion of the rewards registered might get irrevocably lost to the distributor. However, the amount lost should be negligible as long as the condition from 5. is met.
11. **Permissionless nature of the distributor may lead to DOS for popular reward-rewarded token pairs**: The distributor allows anyone to incentivize any token with any reward. A bad actor may grief the party willing to legitimately incentivize a given reward-rewarded token pair by registering a tiny reward stream long before the party decides to register the reward stream themselves. Such a reward stream, if not updated frequently, may lead to a situation where the legitimate party is forced to update it themselves since the time the bad actor set up their stream. This may be costly in terms of gas.

## Install

Expand Down
Loading
Loading