# Technical Interview: Centrifuge <> William
[toc]
## High level description
* Based on extension to paper about scalable block rewards under assumption of constant stake
* Extension solved for changing stakes for a single currency
* Implementation scales by having multiple currencies
* Base idea: Pull based approach which enables accounting reward for one currency in O(1) instead of iterating over all stakers
* By keeping track of stake changes, can derive rewards for epoch from current stake and sum of rates
## Liquidity Rewards requirements
* Complexity: O(1) computation for rewarding a group (sacrifice space to have better time complexity)
* Still `O(|Groups * MaxCurrenciesPerGroup|)` by iterating over Groups on epoch change
* `StakeAccount --> Currency --> Group --> Rate`
* Different reward rates for different groups
* Stakes can be moved from one group to another
* Min duration to claim reward = 1 epoch
* deferred + liquidity rewards = 2 epochs
* Min duration to unlock = 0
* block rewards:
> "The money can not be unstaked"
## Code hierarchy and structure
Copy pasted from spec:
```plantuml
@startuml
skinparam component {
BackgroundColor<<utility>> PaleGoldenRod
BackgroundColor<<with extrinsics>> SkyBlue
}
skinparam storage {
BackgroundColor GoldenRod
}
skinparam rectangle {
BackgroundColor Orange
}
storage "Common types/functions" as common
rectangle "Rewards trait (interface)" as rewards_i
[pallet-rewards] <<utility>> as rewards
[pallet-deferred-rewards] <<utility>> as deferred_rewards
[pallet-liquidity-rewards] <<with extrinsics>> as liquidity
[pallet-block-rewards] <<with extrinsics>> as block
rewards ..> rewards_i : implements
deferred_rewards ..> rewards_i : implements
rewards_i <-- liquidity : use
rewards_i <-- block : use
rewards -up-> common : use
deferred_rewards -up-> common : use
@enduml
```
## Audit/review
### Runtime API
* Reward calculaion should be exposed via runtime API (not as extrinsic!)
* Best practice for claiming multiple currencies would be to handle via runtime API and batch call (see [my comment on PR #944](https://github.com/centrifuge/centrifuge-chain/pull/944#discussion_r999014384)):
1. Do runtime API call to get vec of staked currencies
2. Do batch call for claiming currencies
### Loopholes?
* `pallet-deferred-rewards` substitution to get rid of storing last epoch's rpt seems incorrect, see the bottom of [S15 Appendix A](https://docs.google.com/presentation/d/1EwEcsXVc5HLyULQehz1L-0XMOf7KGuvQWAYVgKcS2Qk/edit#slide=id.g1613d09df7c_0_816):
```diff
- rewarded_amount * rpt == rewarded_amount * rpt-1 - rewarded_amount * rate
+ rewarded_amount * rpt == rewarded_amount * rpt-1 + rewarded_amount * rate
```
$$
\begin{align}
rpt_n & = \sum_{k=1}^{n}\frac{reward_k}{T_k} = rpt_{n-1} + rate \\
& \stackrel{\text{slide 15}}{\ne} rpt_{n-1} - rate
\\ \Leftrightarrow rpt_{n-1} & = rpt_n - rate
\\
\\ \Rightarrow rewarded\_amount * rpt_{n-1}& = rewarded\_amount * ( rpt_n + rate)
\end{align}
$$
* Also sign of `restitution[i]` in `claim` call by implication (see [Appendix A S16](https://docs.google.com/presentation/d/1EwEcsXVc5HLyULQehz1L-0XMOf7KGuvQWAYVgKcS2Qk/edit#slide=id.g1613d09df7c_0_861)):
```diff
- Base::claim_reward(i) - rewarded_stake[i] * rate + restitution[i]
+ Base::claim_reward(i) - rewarded_stake[i] * rate - restitution[i]
```
### Future
* Maximum number of groups for `pallet-liquidity-rewards` and `pallet-block-rewards` critical until WeightsV2, theoretically could exceed PoV size by iterating over BoundedVec in inherent during epoch change (but practically not)
* However, Collator size seems to be planned to stay 16
* Liquidity pools should also stay reasonably small
## How to incentivize longer staking periods?
_Disclaimer: Impracticable, only makes sense in [V2](#V2). However, the majority of the concept of V1 still applies. Did not have time to combine V1 and V2._
_The rewards shall not be paid from the same pot as the epoch rewards. I recommend using the Treasury. However, minting additional rewards into another pot is also an option, but this increases inflation._
### V1
* Add new field `last_claimed` to `StakeAccount` to account for the last epoch in which a reward was claimed
```rust
struct StakeAccount {
stake: Balance,
reward_tally: SignedBalance,
currency_version: u32,
// new, init with 0
last_claimed: u32
}
```
* When claiming a reward, the difference of `current_epoch` and `last_claimed` determines an additional reward rate for longterm holding
* On success (e.g. not zero), `reward * longterm_rate` will be **awarded from a different pot (e.g. Treasury)** to the account which receives rewards in `claim_reward` call
* For every claim, bump `last_claimed` to current epoch
* Add storage item `LongtermRewards`:
```rust
struct LongtermReward {
rate: Rate,
threshold: u32,
}
type LongtermRewards = Map<CurrencyId, LongtermReward>
```
```rust
// Add to `Reward` trait and StakeAccount
// Internally called by `claim_reward`
//
// Iff non-zero rate, `claim_reward` will mint additional
// `reward * longterm_rate` tokens for AccountId from Treasury
fn compute_longterm_reward_rate(currency_id: currency_id, num_epochs: u32) -> Rate {
let longterm_reward =
LongtermRewards::<T>::get(currency_id).ok_or(Error::<T>::CurrenyWithoutLongtermReward)?;
if num_epochs > longterm_reward.threshold {
longterm_reward.rate
} else {
Rate::zero()
}
}
```
```rust
fn claim_reward(account_id: AccountId, currency_id: CurrencyId) -> DispatchResult {
// ...
// Inside StakeAccount::<T>::mutate ...
let longterm_reward: Self::Balance =
compute_longterm_reward_rate(currency_id, staker.last_claimed) * reward;
let treasury = T::LongtermPot::get().into_account_truncating();
if longterm_reward.is_zero() && T::Currency::free_balance(treasury) > longterm_reward {
T::Currency::withdraw(
treasury,
longterm_reward,
WithdrawReasons::TRANSFER,
ExistenceRequirement::KeepAlive,
)?;
}
staker.last_claimed = current_epoch;
// ...
Ok(())
}
```
* Expose set functions in trait `Rewards` which require admin origin in `pallet-liquidity-rewards` and `pallet-block-rewards`
```rust
fn set_longterm_threshold(currency_id: CurrencyId, threshold: u32);
fn set_longterm_rate(currency_id: CurrencyId, rate: Rate);
```
**Q: Period length and reward rate = global setting in `pallet-rewards` or associated to specific Currency/Group?**
* If group: Potential loss/gain when moving Currency to new group
* If currency: Inconsistent with reward rates set for groups
* If global: Inflexible
### Downsides of Proposal:
* Users are disincentivized to call `claim_rewards` before reaching longterm threshold to not miss out on longterm rewards
* However: Longterm rewards could be regarded as opportunity cost of missing out of liquidating/efficiently using earlier received rewards
### V2
* Add a second threshold of type `Balance` to `LongtermReward`
* Only reset `last_claimed` (now moved to a new DoubleStorageMap, see below) if any of the following happens:
* User claimed a longterm reward or
* User staked below the threshold in `withdraw_stake` or
* User claimed with current stake below threshold
* Store sum of claimed rewards which will be multiplied with the longterm rate upon claiming longterm rewards
* This is reset, when the last epoch counter for the user is reset
```rust
struct StakeAccount {
stake: Balance,
reward_tally: SignedBalance,
currency_version: u32,
// new, init with 0
longterm_reward_sum: Balance,
}
```
```rust
struct LongtermReward {
// Reward rate
rate: Rate,
// Minimum required number of epochs in which user must hold min_stake
min_duration: u32,
// Minimum required stake which needs to be hold over min_duration
min_stake: Balance,
}
```
Instead of polluting `StakeAccount` with `last_claimed` epoch, add a new `DoubleStorageMap` to store `last_claimed` of V1, e.g. the last epoch in which the longterm reward was claimed or the stake fell below the `min_stake` threshold.
```rust
type StakeAccountsEpoch = DoubleMap<AccountId, CurrencyId, u32>;
```
#### Downsides
* Have to educate and warn users about risks of staking below threshold
* When `LongtermReward.min_stake`...
* Either have to accept some accounts might be below threshold or
* Have to migrate `StakeAccounts` (no-go!)
____
## Appendix
### WIP: Prove deferred potential incorrect math for Substitution in Appendix (restitution)
* Assume we are in epoch `n` and have a user who first claims claims rewards, then stakes amount X, then unstakes amount Y (!= X) in the same epoch such that `rewarded` in `unstake` is not zero
* In the the next epoch `n+1`, stake is untouched and claim is called again
* Same for the following epoch `n+2`
* Let's compare the reward and tally calculations of Slide 15 with Slide 16
###
$$
\begin{align}
rpt_n & = \sum_{k=1}^{n}\frac{reward_k}{T_k} = rpt_{n-1} + rate \\
& \stackrel{\text{slide 15}}{\ne} rpt_{n-1} - rate
\\ \Leftrightarrow rpt_{n-1} & = rpt_n - rate \tag{1}
\\ \Rightarrow rewarded\_amount * rpt_{n-1}& = rewarded\_amount * ( rpt_n + rate)
\end{align}
$$
### V1: Using "old rate" from [S15 Appendix A](https://docs.google.com/presentation/d/1EwEcsXVc5HLyULQehz1L-0XMOf7KGuvQWAYVgKcS2Qk/edit#slide=id.g1613d09df7c_0_816)
TODO: Why does this resolve to `-rewarded * rate`?
$$
\begin{align}
tally[i] & = stake[i] * rpt - rewarded\_stake[i] * rate + rewarded\_amount * rate
\\ reward[i] & = stake[i] * rpt - tally[i] - rewarded\_stake[i] * rate
\\ & = stake[i] * rpt - stake[i] * rpt + rewarded\_stake[i] * rate - rewarded\_amount * rate
\\ & - rewarded\_stake[i] * rate
\\ & = - rewarded * rate \tag{V1}
\end{align}
$$
### V2: Using restitution from [S16 Appendix A](https://docs.google.com/presentation/d/1EwEcsXVc5HLyULQehz1L-0XMOf7KGuvQWAYVgKcS2Qk/edit#slide=id.g1613d09df7c_0_861)
$$
\begin{align}
tally[i] & = stake[i] * rpt
\\ restitution[i] & = -rewarded\_stake[i] * rate + rewarded\_amount * rate
\\ reward[i] & = stake[i] * rpt - tally[i] - rewarded\_stake[i] * rate + restitution[i]
\\ & = stake[i] * rpt - stake[i] * rpt - rewarded\_stake[i] * rate
\\ & - rewarded\_stake[i] * rate + rewarded\_amount * rate
\\ & = -2 * rewared\_stake[i] + rewarded\_amount
\\ & \stackrel{V1}{\neq} - rewarded * rate
\\
\\ \Rightarrow reward[i] & \stackrel{!}{=} stake[i] * rpt - tally[i] - rewarded\_stake[i] * rate \color{red}{-} restitution[i]
\end{align}
$$