Skip to content

Commit 443ed51

Browse files
rewards improvements (#760)
* added reward pool example in python * cleaned up python * cleaned up tests * starting prove * spelling * prove of rewards maths * claim single reward * fmt * more code reuse in rewards * Update rewards/README.md Co-authored-by: zqhxuyuan <[email protected]> * Update rewards/README.md Co-authored-by: zqhxuyuan <[email protected]> * Update rewards/README.md Co-authored-by: zqhxuyuan <[email protected]> * Update rewards/README.md Co-authored-by: zqhxuyuan <[email protected]> * Update rewards/README.md Co-authored-by: zqhxuyuan <[email protected]> * fixed clippy Co-authored-by: zqhxuyuan <[email protected]>
1 parent 2a7183f commit 443ed51

File tree

3 files changed

+137
-28
lines changed

3 files changed

+137
-28
lines changed

rewards/README.md

+32-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This module exposes capabilities for staking rewards.
66

77
### Single asset algorithm
88

9-
If to consider single pool with single reward asset, generally it will behave as next:
9+
If consider a single pool with a single reward asset, generally it will behave as next:
1010

1111
```python
1212
from collections import defaultdict
@@ -23,10 +23,10 @@ def inflate(pool, user_share):
2323

2424
def add_share(pool, users, user, user_share):
2525
# virtually we add more rewards, but claim they were claimed by user
26-
# so until `rewards` grows, users will not be able to claim more than zero
26+
# so until `rewards` grows, user will not be able to claim more than zero
2727
to_withdraw = inflate(pool, user_share)
28-
pool["rewards"] = pool["rewards"] + to_withdraw
29-
pool["withdrawn_rewards"] = pool["withdrawn_rewards"] + to_withdraw
28+
pool["rewards"] = pool["rewards"] + to_withdraw
29+
pool["withdrawn_rewards"] = pool["withdrawn_rewards"] + to_withdraw
3030
pool["shares"] += user_share
3131
user = users[user]
3232
user["shares"] += user_share
@@ -43,3 +43,31 @@ def claim_rewards(pool, users, user):
4343
user["withdrawn_rewards"] += to_withdraw
4444
return to_withdraw
4545
```
46+
47+
### Prove
48+
49+
We want to prove that when a new share is added, it does not dilute previous rewards.
50+
51+
The user who adds a share after the reward is accumulated, will not get any part of the previous reward.
52+
53+
Let $R_n$ be the amount of the current reward asset.
54+
55+
Let $s_i$ be the stake of any specific user our of $m$ total users.
56+
57+
User current reward share equals $$r_i = R_n * ({s_i} / {\sum_{i=1}^m s_i}) $$
58+
59+
User $m + 1$ brings his share, so $$r_i' = R_n * ({s_i} / {\sum_{i=1}^{m+1} s_i}) $$
60+
61+
$r_i > r_i'$, so the original share was diluted and a new user can claim the share of existing users.
62+
63+
What if we increase $R_n$ by $\delta_R$ so that original users get the same share.
64+
65+
We get:
66+
67+
$$ R_n * ({s_i} / {\sum_{i=1}^m s_i}) = ({R_n + \delta_R}) * ({s_i} / {\sum_{i=1}^{m+1} s_i})$$
68+
69+
After easy to do algebraic simplification we get
70+
71+
$$ \delta_R = R_n * ({s_m}/{\sum_{i=1}^{m} s_i}) $$
72+
73+
So for new share we increase reward pool. To compensate for that $\delta_R$ amount is marked as withdrawn from pool by new user.

rewards/src/lib.rs

+82-24
Original file line numberDiff line numberDiff line change
@@ -272,34 +272,92 @@ impl<T: Config> Pallet<T> {
272272
let total_shares = U256::from(pool_info.total_shares.to_owned().saturated_into::<u128>());
273273
pool_info.rewards.iter_mut().for_each(
274274
|(reward_currency, (total_reward, total_withdrawn_reward))| {
275-
let withdrawn_reward = withdrawn_rewards.get(reward_currency).copied().unwrap_or_default();
276-
277-
let total_reward_proportion: T::Balance =
278-
U256::from(share.to_owned().saturated_into::<u128>())
279-
.saturating_mul(U256::from(total_reward.to_owned().saturated_into::<u128>()))
280-
.checked_div(total_shares)
281-
.unwrap_or_default()
282-
.as_u128()
283-
.unique_saturated_into();
284-
285-
let reward_to_withdraw = total_reward_proportion
286-
.saturating_sub(withdrawn_reward)
287-
.min(total_reward.saturating_sub(*total_withdrawn_reward));
288-
289-
if reward_to_withdraw.is_zero() {
290-
return;
291-
}
292-
293-
*total_withdrawn_reward = total_withdrawn_reward.saturating_add(reward_to_withdraw);
294-
withdrawn_rewards
295-
.insert(*reward_currency, withdrawn_reward.saturating_add(reward_to_withdraw));
296-
297-
// pay reward to `who`
298-
T::Handler::payout(who, pool, *reward_currency, reward_to_withdraw);
275+
Self::claim_one(
276+
withdrawn_rewards,
277+
*reward_currency,
278+
share,
279+
total_reward,
280+
total_shares,
281+
total_withdrawn_reward,
282+
who,
283+
pool,
284+
);
299285
},
300286
);
301287
});
302288
}
303289
});
304290
}
291+
292+
pub fn claim_reward(who: &T::AccountId, pool: &T::PoolId, reward_currency: T::CurrencyId) {
293+
SharesAndWithdrawnRewards::<T>::mutate_exists(pool, who, |maybe_share_withdrawn| {
294+
if let Some((share, withdrawn_rewards)) = maybe_share_withdrawn {
295+
if share.is_zero() {
296+
return;
297+
}
298+
299+
PoolInfos::<T>::mutate(pool, |pool_info| {
300+
let total_shares = U256::from(pool_info.total_shares.to_owned().saturated_into::<u128>());
301+
if let Some((total_reward, total_withdrawn_reward)) = pool_info.rewards.get_mut(&reward_currency) {
302+
Self::claim_one(
303+
withdrawn_rewards,
304+
reward_currency,
305+
share,
306+
total_reward,
307+
total_shares,
308+
total_withdrawn_reward,
309+
who,
310+
pool,
311+
);
312+
}
313+
});
314+
}
315+
});
316+
}
317+
318+
#[allow(clippy::too_many_arguments)] // just we need to have all these to do the stuff
319+
fn claim_one(
320+
withdrawn_rewards: &mut BTreeMap<T::CurrencyId, T::Balance>,
321+
reward_currency: T::CurrencyId,
322+
share: &mut T::Share,
323+
total_reward: &mut T::Balance,
324+
total_shares: U256,
325+
total_withdrawn_reward: &mut T::Balance,
326+
who: &T::AccountId,
327+
pool: &T::PoolId,
328+
) {
329+
let withdrawn_reward = withdrawn_rewards.get(&reward_currency).copied().unwrap_or_default();
330+
let reward_to_withdraw = Self::reward_to_withdraw(
331+
share,
332+
total_reward,
333+
total_shares,
334+
withdrawn_reward,
335+
total_withdrawn_reward,
336+
);
337+
if !reward_to_withdraw.is_zero() {
338+
*total_withdrawn_reward = total_withdrawn_reward.saturating_add(reward_to_withdraw);
339+
withdrawn_rewards.insert(reward_currency, withdrawn_reward.saturating_add(reward_to_withdraw));
340+
341+
// pay reward to `who`
342+
T::Handler::payout(who, pool, reward_currency, reward_to_withdraw);
343+
}
344+
}
345+
346+
fn reward_to_withdraw(
347+
share: &mut T::Share,
348+
total_reward: &mut T::Balance,
349+
total_shares: U256,
350+
withdrawn_reward: T::Balance,
351+
total_withdrawn_reward: &mut T::Balance,
352+
) -> T::Balance {
353+
let total_reward_proportion: T::Balance = U256::from(share.to_owned().saturated_into::<u128>())
354+
.saturating_mul(U256::from(total_reward.to_owned().saturated_into::<u128>()))
355+
.checked_div(total_shares)
356+
.unwrap_or_default()
357+
.as_u128()
358+
.unique_saturated_into();
359+
total_reward_proportion
360+
.saturating_sub(withdrawn_reward)
361+
.min(total_reward.saturating_sub(*total_withdrawn_reward))
362+
}
305363
}

rewards/src/tests.rs

+23
Original file line numberDiff line numberDiff line change
@@ -507,3 +507,26 @@ fn share_to_zero_removes_storage() {
507507
assert_eq!(SharesAndWithdrawnRewards::<Runtime>::contains_key(DOT_POOL, BOB), false);
508508
});
509509
}
510+
511+
#[test]
512+
fn claim_single_reward() {
513+
ExtBuilder::default().build().execute_with(|| {
514+
assert_eq!(RewardsModule::pool_infos(DOT_POOL), PoolInfo::default());
515+
516+
RewardsModule::add_share(&ALICE, &DOT_POOL, 100);
517+
518+
assert_ok!(RewardsModule::accumulate_reward(&DOT_POOL, NATIVE_COIN, 100));
519+
assert_ok!(RewardsModule::accumulate_reward(&DOT_POOL, STABLE_COIN, 200));
520+
RewardsModule::claim_reward(&ALICE, &DOT_POOL, STABLE_COIN);
521+
522+
assert_eq!(
523+
RewardsModule::pool_infos(DOT_POOL),
524+
PoolInfo {
525+
total_shares: 100,
526+
rewards: vec![(NATIVE_COIN, (100, 0)), (STABLE_COIN, (200, 200))]
527+
.into_iter()
528+
.collect(),
529+
}
530+
);
531+
});
532+
}

0 commit comments

Comments
 (0)