Skip to content

Commit adc4fee

Browse files
rewards split (#771)
* make renders work with new line * sometimes shares should be split or transfered to new account * added split test with prove of withdrawals * removding mult by ration and use u256 directly * fixed split * ensure of option
1 parent 463635e commit adc4fee

3 files changed

Lines changed: 96 additions & 7 deletions

File tree

rewards/README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
# Rewards module
22

3-
### Overview
4-
53
This module exposes capabilities for staking rewards.
64

7-
### Single asset algorithm
5+
## Single asset algorithm
86

9-
If consider a single pool with a single reward asset, generally it will behave as next:
7+
Consider a single pool with a single reward asset, generally, it will behave as next:
108

119
```python
1210
from collections import defaultdict
@@ -54,9 +52,13 @@ Let $R_n$ be the amount of the current reward asset.
5452

5553
Let $s_i$ be the stake of any specific user our of $m$ total users.
5654

57-
User current reward share equals $$r_i = R_n * ({s_i} / {\sum_{i=1}^m s_i}) $$
55+
User current reward share equals
56+
57+
$$ r_i = R_n * ({s_i} / {\sum_{i=1}^m s_i}) $$
58+
59+
User $m + 1$ brings his share, so
5860

59-
User $m + 1$ brings his share, so $$r_i' = R_n * ({s_i} / {\sum_{i=1}^{m+1} s_i}) $$
61+
$$r_i' = R_n * ({s_i} / {\sum_{i=1}^{m+1} s_i}) $$
6062

6163
$r_i > r_i'$, so the original share was diluted and a new user can claim the share of existing users.
6264

rewards/src/lib.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub use module::*;
4343

4444
#[frame_support::pallet]
4545
pub mod module {
46+
4647
use super::*;
4748

4849
#[pallet::config]
@@ -76,10 +77,14 @@ pub mod module {
7677
type Handler: RewardHandler<Self::AccountId, Self::CurrencyId, Balance = Self::Balance, PoolId = Self::PoolId>;
7778
}
7879

80+
type WithdrawnRewards<T> = BTreeMap<<T as Config>::CurrencyId, <T as Config>::Balance>;
81+
7982
#[pallet::error]
8083
pub enum Error<T> {
8184
/// Pool does not exist
8285
PoolDoesNotExist,
86+
ShareDoesNotExist,
87+
CanSplitOnlyLessThanShare,
8388
}
8489

8590
/// Record reward pool info.
@@ -102,7 +107,7 @@ pub mod module {
102107
T::PoolId,
103108
Twox64Concat,
104109
T::AccountId,
105-
(T::Share, BTreeMap<T::CurrencyId, T::Balance>),
110+
(T::Share, WithdrawnRewards<T>),
106111
ValueQuery,
107112
>;
108113

@@ -316,6 +321,51 @@ impl<T: Config> Pallet<T> {
316321
});
317322
}
318323

324+
/// Splits share into two parts.
325+
///
326+
/// `move_share` - amount of share to remove and put into `other` share
327+
/// `other` - new account who will own new share
328+
///
329+
/// Similar too claim and add 2 shares later, but does not requires pool
330+
/// inflation and is more efficient.
331+
pub fn transfer_share_and_rewards(
332+
who: &T::AccountId,
333+
pool: &T::PoolId,
334+
move_share: T::Share,
335+
other: &T::AccountId,
336+
) -> DispatchResult {
337+
SharesAndWithdrawnRewards::<T>::mutate(pool, other, |increased_share| {
338+
let (increased_share, increased_rewards) = increased_share;
339+
SharesAndWithdrawnRewards::<T>::mutate_exists(pool, who, |share| {
340+
let (share, rewards) = share.as_mut().ok_or(Error::<T>::ShareDoesNotExist)?;
341+
ensure!(move_share < *share, Error::<T>::CanSplitOnlyLessThanShare);
342+
for (reward_currency, balance) in rewards {
343+
// u128 * u128 is always less than u256
344+
// move_share / share always less then 1 and share > 0
345+
// so final results is computable and is always less or equal than u128
346+
let move_balance = U256::from(balance.to_owned().saturated_into::<u128>())
347+
* U256::from(move_share.to_owned().saturated_into::<u128>())
348+
/ U256::from(share.to_owned().saturated_into::<u128>());
349+
let move_balance: Option<u128> = move_balance.try_into().ok();
350+
if let Some(move_balance) = move_balance {
351+
let move_balance: T::Balance = move_balance.unique_saturated_into();
352+
*balance = balance.saturating_sub(move_balance);
353+
increased_rewards
354+
.entry(*reward_currency)
355+
.and_modify(|increased_reward| {
356+
*increased_reward = increased_reward.saturating_add(move_balance);
357+
})
358+
.or_insert(move_balance);
359+
}
360+
}
361+
*share = share.saturating_sub(move_share);
362+
*increased_share = increased_share.saturating_add(move_share);
363+
Ok(())
364+
})
365+
})
366+
}
367+
368+
#[allow(clippy::too_many_arguments)] // just we need to have all these to do the stuff
319369
fn claim_one(
320370
withdrawn_rewards: &mut BTreeMap<T::CurrencyId, T::Balance>,
321371
reward_currency: T::CurrencyId,

rewards/src/tests.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,3 +530,40 @@ fn claim_single_reward() {
530530
);
531531
});
532532
}
533+
534+
#[test]
535+
fn transfer_share_and_rewards() {
536+
ExtBuilder::default().build().execute_with(|| {
537+
RewardsModule::add_share(&ALICE, &DOT_POOL, 100);
538+
assert_ok!(RewardsModule::accumulate_reward(&DOT_POOL, NATIVE_COIN, 100));
539+
RewardsModule::add_share(&BOB, &DOT_POOL, 100);
540+
let pool_info = RewardsModule::pool_infos(DOT_POOL);
541+
assert_ok!(RewardsModule::transfer_share_and_rewards(&ALICE, &DOT_POOL, 33, &BOB));
542+
assert_ok!(RewardsModule::transfer_share_and_rewards(&ALICE, &DOT_POOL, 33, &CAROL));
543+
let new_pool_info = RewardsModule::pool_infos(DOT_POOL);
544+
assert_eq!(pool_info, new_pool_info, "reward transfer does not affect the pool");
545+
546+
assert_eq!(
547+
RewardsModule::shares_and_withdrawn_rewards(DOT_POOL, ALICE),
548+
(34, Default::default())
549+
);
550+
assert_eq!(
551+
RewardsModule::shares_and_withdrawn_rewards(DOT_POOL, BOB),
552+
(133, vec![(NATIVE_COIN, 100)].into_iter().collect())
553+
);
554+
assert_eq!(
555+
RewardsModule::shares_and_withdrawn_rewards(DOT_POOL, CAROL),
556+
(33, Default::default())
557+
);
558+
assert_ok!(RewardsModule::transfer_share_and_rewards(&BOB, &DOT_POOL, 10, &CAROL));
559+
assert_eq!(
560+
RewardsModule::shares_and_withdrawn_rewards(DOT_POOL, CAROL),
561+
(43, vec![(NATIVE_COIN, 100 * 10 / 133)].into_iter().collect())
562+
);
563+
564+
assert_noop!(
565+
RewardsModule::transfer_share_and_rewards(&CAROL, &DOT_POOL, 1000, &ALICE),
566+
Error::<Runtime>::CanSplitOnlyLessThanShare
567+
);
568+
});
569+
}

0 commit comments

Comments
 (0)