diff --git a/x/incentive/spec/01_concepts.md b/x/incentive/spec/01_concepts.md index c5089038..efc8678c 100644 --- a/x/incentive/spec/01_concepts.md +++ b/x/incentive/spec/01_concepts.md @@ -4,23 +4,85 @@ order: 1 # Concepts -This module presents an implementation of user incentives that are controlled by governance. When users take a certain action, for example opening a CDP, they become eligible for rewards. Rewards are **opt in** meaning that users must submit a message before the claim deadline to claim their rewards. The goals and background of this module were subject of a previous Kava governance proposal, which can be found [here](https://ipfs.io/ipfs/QmSYedssC3nyQacDJmNcREtgmTPyaMx2JX7RNkMdAVkdkr/user-growth-fund-proposal.pdf) +This module implements governance controlled user incentives. When users take a certain action, for example opening a CDP, they become eligible for rewards. Rewards are **opt in** meaning that users must submit a message before the claim deadline to claim their rewards. The goals and background of this module were subject of a previous Kava governance proposal, which can be found [here](https://ipfs.io/ipfs/QmSYedssC3nyQacDJmNcREtgmTPyaMx2JX7RNkMdAVkdkr/user-growth-fund-proposal.pdf) ## General Reward Distribution -Rewards target various user activity. For example, usdx borrowed from bnb CDPs, btcb supplied to the hard money market, or owned shares in a swap kava/usdx pool. +Rewards target various user activity. For example, usdx borrowed from bnb CDPs, btcb supplied to the hard money market, or shares owned in a swap kava/usdx pool. Each second, the rewards accumulate at a rate set in the params, eg 100 ukava per second. These are then distributed to all users ratably based on their percentage involvement in the rewarded activity. For example if a user holds 1% of all funds deposited to the kava/usdx swap pool. They will receive 1% of the total rewards each second. -The quantity tracking a user's involvement is referred to as "source shares" in the code. And the total across all users the "total source shares". The quotient then gives their percentage involvement, eg if a user borrowed 10,000 usdx, and there is 100,000 usdx borrowed by all users, then they will get 10% of rewards. +The quantity tracking a user's involvement is referred to as "source shares". And the total across all users the "total source shares". The quotient then gives their percentage involvement, eg if a user borrowed 10,000 usdx, and there is 100,000 usdx borrowed by all users, then they will get 10% of rewards. ## Efficiency -Paying out rewards to every user every block would be slow and lead to long block times. Instead rewards are calculated much less frequently. +Paying out rewards to every user every block would be slow and lead to long block times. Instead rewards are calculated lazily only when needed. -Every block a global tracker adds up total rewards paid out per unit of user involvement. For example, per unit of xrpb supplied to hard, or per share in a kava/usdx swap pool. A user's specific reward can then be calculated as needed based on their current source shares. +First, every block, the amount of rewards to be distributed in that block are divided by the total source shares to get the rewards per share. This is added to a global total (named "global indexes"). This is repeated every block such that the global indexes represents the total rewards a user should be owed per source share if they had held a deposit from when the rewards were created. -Users' rewards must be updated whenever their source shares change. This happens through hooks into other modules that run before deposits/borrows/supplies etc. +Then, if a user has deposited (say into a CDP) at the very start of the chain (and never changed their deposit), their current reward balance can be calculated at any time $t$ as + +$$ +\texttt{rewards}_ t = \texttt{globalIndexes}_ t \cdot \texttt{sourceShares}_ t +$$ + +If a user modifies their source shares (at say time $t-10$) we can still calculate their total rewards: + +$$ +\texttt{rewards}_ t= \text{rewards accrued up to time t-10} + \text{rewards accrued from time t-10 to time t} +$$ + +$$ +\texttt{rewards}_ t = \texttt{globalIndexes}_ {t-10} \cdot \texttt{sourceShares}_ {t-10} + (\texttt{globalIndexes}_ t - \texttt{globalIndexes}_ {t-10}) \cdot \texttt{sourceShares}_ t +$$ + +This generalizes to any number of source share modifications. + +In code, to avoid storing the entire history of a user's source shares and global index values, rewards are calculated on every source shares change and added to a reward balance: + +$$ +\texttt{rewards}_ t = \texttt{rewardBalance}_ {t -10} + (\texttt{globalIndexes}_ t - \texttt{globalIndexes}_ {t-10}) \cdot \texttt{sourceShares}_ t +$$ + +Old values of $\texttt{rewardBalance}$ and $\texttt{globalIndexes}$ ares stored in a `Claim` object for each user as `rewardBalance` and `rewardIndexes` respectively. + +Listeners on external modules fire to update these values when source shares change. For example, when a user deposits to hard, a method in incentive is called. This fundamental operation is called "sync". It calculates the rewards accrued since last time the `sourceShares` changed, adds it to the claim, and stores the current `globalIndexes` in the `rewardIndexes`. Sync must be called whenever source shares change, otherwise incorrect rewards will be distributed. + +Enumeration of 'sync' input states: +- `sourceShares`, `globalIndexes`, or `rewardIndexes` should never be negative +- `globalIndexes` >= `rewardIndexes` (global indexes must never decrease) +- `globalIndexes` and `rewardIndexes` can be positive or 0, where not existing in the store is counted as 0 + +- `sourceShares` are the value before the update (eg before a hard deposit) + + | `globalIndexes` | `rewardIndexes` | `sourceShares` | description | + |------------------|-----------------|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + | positive | positive | positive | normal sync | + | positive | positive | 0 | normal (can happen when a user is creating a deposit (so shares are increasing from 0)) | + | positive | 0 | positive | The claim doesn't hold indexes, so the global indexes must have been added since last sync (eg a new denom was added to reward params). This is indistinguishable from a claim accidentally being deleted, where it will accrue a large amount of rewards. | + | positive | 0 | 0 | User is creating source shares. | + | 0 | positive | positive | global indexes < claim indexes - fatal error, otherwise the new rewards will be negative | + | 0 | positive | 0 | global indexes < claim indexes - fatal error | + | 0 | 0 | positive | Source has no rewards yet. User is updating their shares. | + | 0 | 0 | 0 | Source has no rewards yet. User is creating source shares. | + +It is important that: +- claim indexes are not deleted + - Otherwise when sync is called, it will fill them in with 0 values and perform sync as if the user had deposit since the beginning of the rewards (usually accumulating a lot of rewards). +- global indexes are not deleted + - Otherwise claims cannot be synced. Problematic if a sync happens in a begin blocker and it panics. +- hooks are called any time source shares change + - If source shares can be updated without a sync, it can be possible to accumulate far too much rewards. For example, a user who holds a small deposit for a long time could deposit a large amount and skip the sync, then trigger a sync which will calculate rewards as if the large deposit was there for a long time. + +The code is further complicated by: +- Claim objects contain indexes for several source shares. +- Rewards for hard borrows and hard deposits use the same claim object. +- Savings and hard hooks trigger any time one in a group of source shares change, but don't identify which changed. +- The hard `BeforeXModified` hooks don't show source shares that have increased from zero (eg when a new denom is deposited to an existing deposit). So there is an additional `AfterXModified` hook, and the claim indexes double up as a copy of the borrow/deposit denoms. +- The sync operation is split between two methods to try to protect against indexes being deleted. + - `InitXRewards` performs a sync assuming source shares are 0, it mostly fires in cases where `sourceShares` = 0 above (except for hard and supply) + - `SyncXRewards` performs a sync, but skips it if `globalIndexes` are not found or `rewardIndexes` are not found (only when claim object not found) +- Usdx rewards do not support multiple reward denoms. ## HARD Token distribution