Skip to content

Commit fc4496e

Browse files
support multi currency rewards (#601)
* support multi currency rewards * update migration * remove storage_version * fixes * add missing import * update docs * add missing import * fix currency type * make sure there're no empty entries * import prelude::* * update std import * accumulate reward returns error if pool does not exist * cleanup * cleanup * move PoolInfoV0 to migration * test migration * update migration test * keep old map storage for migrate keys * new migration * fix migration * update * limit migration * fix Co-authored-by: wangjj9219 <[email protected]>
1 parent d0f9033 commit fc4496e

File tree

6 files changed

+722
-229
lines changed

6 files changed

+722
-229
lines changed

rewards/Cargo.toml

+2-3
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,11 @@ codec = { package = "parity-scale-codec", version = "2.2.0", default-features =
1313
sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.9", default-features = false }
1414
sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.9", default-features = false }
1515
sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.9", default-features = false }
16+
sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.9", default-features = false }
1617
frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.9", default-features = false }
1718
frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.9", default-features = false }
1819
orml-traits = { path = "../traits", version = "0.4.1-dev", default-features = false }
1920

20-
[dev-dependencies]
21-
sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.9" }
22-
2321
[features]
2422
default = ["std"]
2523
std = [
@@ -28,6 +26,7 @@ std = [
2826
"sp-runtime/std",
2927
"sp-io/std",
3028
"sp-std/std",
29+
"sp-core/std",
3130
"frame-support/std",
3231
"frame-system/std",
3332
"orml-traits/std",

rewards/src/lib.rs

+198-77
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,42 @@
11
#![allow(clippy::unused_unit)]
22
#![cfg_attr(not(feature = "std"), no_std)]
33

4+
pub mod migrations;
45
mod mock;
56
mod tests;
67

78
use codec::{FullCodec, HasCompact, MaxEncodedLen};
8-
use frame_support::pallet_prelude::*;
9+
use frame_support::{pallet_prelude::*, weights::Weight};
10+
pub use migrations::PoolInfoV0;
11+
use orml_traits::RewardHandler;
12+
use sp_core::U256;
913
use sp_runtime::{
10-
traits::{AtLeast32BitUnsigned, Bounded, MaybeSerializeDeserialize, Member, Saturating, Zero},
11-
FixedPointNumber, FixedPointOperand, FixedU128, RuntimeDebug,
12-
};
13-
use sp_std::{
14-
cmp::{Eq, PartialEq},
15-
fmt::Debug,
14+
traits::{AtLeast32BitUnsigned, Convert, MaybeSerializeDeserialize, Member, Saturating, UniqueSaturatedInto, Zero},
15+
FixedPointOperand, RuntimeDebug, SaturatedConversion,
1616
};
17-
18-
use orml_traits::RewardHandler;
17+
use sp_std::{borrow::ToOwned, collections::btree_map::BTreeMap, fmt::Debug, prelude::*};
1918

2019
/// The Reward Pool Info.
21-
#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, Default, MaxEncodedLen)]
22-
pub struct PoolInfo<Share: HasCompact, Balance: HasCompact> {
20+
#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug)]
21+
pub struct PoolInfo<Share: HasCompact, Balance: HasCompact, CurrencyId: Ord> {
2322
/// Total shares amount
24-
#[codec(compact)]
2523
pub total_shares: Share,
26-
/// Total rewards amount
27-
#[codec(compact)]
28-
pub total_rewards: Balance,
29-
/// Total withdrawn rewards amount
30-
#[codec(compact)]
31-
pub total_withdrawn_rewards: Balance,
24+
/// Reward infos <reward_currency, (total_reward, total_withdrawn_reward)>
25+
pub rewards: BTreeMap<CurrencyId, (Balance, Balance)>,
26+
}
27+
28+
impl<Share, Balance, CurrencyId> Default for PoolInfo<Share, Balance, CurrencyId>
29+
where
30+
Share: Default + HasCompact,
31+
Balance: HasCompact,
32+
CurrencyId: Ord,
33+
{
34+
fn default() -> Self {
35+
Self {
36+
total_shares: Default::default(),
37+
rewards: BTreeMap::new(),
38+
}
39+
}
3240
}
3341

3442
pub use module::*;
@@ -59,24 +67,64 @@ pub mod module {
5967
+ Debug
6068
+ FixedPointOperand;
6169

70+
/// The old version of reward pool ID type.
71+
/// NOTE: remove it after migration
72+
type PoolIdV0: Parameter + Member + Clone + FullCodec;
73+
74+
/// The convertor to convert PoolIdV0 to PoolId
75+
/// NOTE: remove it after migration
76+
type PoolIdConvertor: Convert<Self::PoolIdV0, Option<Self::PoolId>>;
77+
6278
/// The reward pool ID type.
6379
type PoolId: Parameter + Member + Clone + FullCodec;
6480

81+
type CurrencyId: Parameter + Member + Copy + MaybeSerializeDeserialize + Ord;
82+
6583
/// The `RewardHandler`
66-
type Handler: RewardHandler<Self::AccountId, Balance = Self::Balance, PoolId = Self::PoolId>;
84+
type Handler: RewardHandler<Self::AccountId, Self::CurrencyId, Balance = Self::Balance, PoolId = Self::PoolId>;
85+
}
86+
87+
#[pallet::error]
88+
pub enum Error<T> {
89+
/// Pool does not exist
90+
PoolDoesNotExist,
6791
}
6892

6993
/// Stores reward pool info.
94+
/// NOTE: remove it after migration
7095
#[pallet::storage]
7196
#[pallet::getter(fn pools)]
72-
pub type Pools<T: Config> = StorageMap<_, Twox64Concat, T::PoolId, PoolInfo<T::Share, T::Balance>, ValueQuery>;
97+
pub type Pools<T: Config> = StorageMap<_, Twox64Concat, T::PoolIdV0, PoolInfoV0<T::Share, T::Balance>, ValueQuery>;
98+
99+
/// Record reward pool info.
100+
#[pallet::storage]
101+
#[pallet::getter(fn pool_infos)]
102+
pub type PoolInfos<T: Config> =
103+
StorageMap<_, Twox64Concat, T::PoolId, PoolInfo<T::Share, T::Balance, T::CurrencyId>, ValueQuery>;
73104

74105
/// Record share amount and withdrawn reward amount for specific `AccountId`
75106
/// under `PoolId`.
107+
/// NOTE: remove it after migration
76108
#[pallet::storage]
77109
#[pallet::getter(fn share_and_withdrawn_reward)]
78110
pub type ShareAndWithdrawnReward<T: Config> =
79-
StorageDoubleMap<_, Twox64Concat, T::PoolId, Twox64Concat, T::AccountId, (T::Share, T::Balance), ValueQuery>;
111+
StorageDoubleMap<_, Twox64Concat, T::PoolIdV0, Twox64Concat, T::AccountId, (T::Share, T::Balance), ValueQuery>;
112+
113+
/// Record share amount, reward currency and withdrawn reward amount for
114+
/// specific `AccountId` under `PoolId`.
115+
///
116+
/// double_map (PoolId, AccountId) => (Share, BTreeMap<CurrencyId, Balance>)
117+
#[pallet::storage]
118+
#[pallet::getter(fn shares_and_withdrawn_rewards)]
119+
pub type SharesAndWithdrawnRewards<T: Config> = StorageDoubleMap<
120+
_,
121+
Twox64Concat,
122+
T::PoolId,
123+
Twox64Concat,
124+
T::AccountId,
125+
(T::Share, BTreeMap<T::CurrencyId, T::Balance>),
126+
ValueQuery,
127+
>;
80128

81129
#[pallet::pallet]
82130
pub struct Pallet<T>(_);
@@ -89,35 +137,73 @@ pub mod module {
89137
}
90138

91139
impl<T: Config> Pallet<T> {
92-
pub fn accumulate_reward(pool: &T::PoolId, reward_increment: T::Balance) {
93-
if !reward_increment.is_zero() {
94-
Pools::<T>::mutate(pool, |pool_info| {
95-
pool_info.total_rewards = pool_info.total_rewards.saturating_add(reward_increment)
96-
});
140+
pub fn accumulate_reward(
141+
pool: &T::PoolId,
142+
reward_currency: T::CurrencyId,
143+
reward_increment: T::Balance,
144+
) -> DispatchResult {
145+
if reward_increment.is_zero() {
146+
return Ok(());
97147
}
148+
PoolInfos::<T>::mutate_exists(pool, |maybe_pool_info| -> DispatchResult {
149+
let pool_info = maybe_pool_info.as_mut().ok_or(Error::<T>::PoolDoesNotExist)?;
150+
151+
pool_info
152+
.rewards
153+
.entry(reward_currency)
154+
.and_modify(|(total_reward, _)| {
155+
*total_reward = total_reward.saturating_add(reward_increment);
156+
})
157+
.or_insert((reward_increment, Zero::zero()));
158+
159+
Ok(())
160+
})
98161
}
99162

100163
pub fn add_share(who: &T::AccountId, pool: &T::PoolId, add_amount: T::Share) {
101164
if add_amount.is_zero() {
102165
return;
103166
}
104167

105-
Pools::<T>::mutate(pool, |pool_info| {
106-
let reward_inflation = if pool_info.total_shares.is_zero() {
107-
Zero::zero()
108-
} else {
109-
let proportion = FixedU128::checked_from_rational(add_amount, pool_info.total_shares)
110-
.unwrap_or_else(FixedU128::max_value);
111-
proportion.saturating_mul_int(pool_info.total_rewards)
112-
};
113-
168+
PoolInfos::<T>::mutate(pool, |pool_info| {
169+
let initial_total_shares = pool_info.total_shares;
114170
pool_info.total_shares = pool_info.total_shares.saturating_add(add_amount);
115-
pool_info.total_rewards = pool_info.total_rewards.saturating_add(reward_inflation);
116-
pool_info.total_withdrawn_rewards = pool_info.total_withdrawn_rewards.saturating_add(reward_inflation);
117171

118-
ShareAndWithdrawnReward::<T>::mutate(pool, who, |(share, withdrawn_rewards)| {
172+
let mut withdrawn_inflation = Vec::<(T::CurrencyId, T::Balance)>::new();
173+
174+
pool_info
175+
.rewards
176+
.iter_mut()
177+
.for_each(|(reward_currency, (total_reward, total_withdrawn_reward))| {
178+
let reward_inflation = if initial_total_shares.is_zero() {
179+
Zero::zero()
180+
} else {
181+
U256::from(add_amount.to_owned().saturated_into::<u128>())
182+
.saturating_mul(total_reward.to_owned().saturated_into::<u128>().into())
183+
.checked_div(initial_total_shares.to_owned().saturated_into::<u128>().into())
184+
.unwrap_or_default()
185+
.as_u128()
186+
.saturated_into()
187+
};
188+
*total_reward = total_reward.saturating_add(reward_inflation);
189+
*total_withdrawn_reward = total_withdrawn_reward.saturating_add(reward_inflation);
190+
191+
withdrawn_inflation.push((*reward_currency, reward_inflation));
192+
});
193+
194+
SharesAndWithdrawnRewards::<T>::mutate(pool, who, |(share, withdrawn_rewards)| {
119195
*share = share.saturating_add(add_amount);
120-
*withdrawn_rewards = withdrawn_rewards.saturating_add(reward_inflation);
196+
// update withdrawn inflation for each reward currency
197+
withdrawn_inflation
198+
.into_iter()
199+
.for_each(|(reward_currency, reward_inflation)| {
200+
withdrawn_rewards
201+
.entry(reward_currency)
202+
.and_modify(|withdrawn_reward| {
203+
*withdrawn_reward = withdrawn_reward.saturating_add(reward_inflation);
204+
})
205+
.or_insert(reward_inflation);
206+
});
121207
});
122208
});
123209
}
@@ -130,26 +216,50 @@ impl<T: Config> Pallet<T> {
130216
// claim rewards firstly
131217
Self::claim_rewards(who, pool);
132218

133-
ShareAndWithdrawnReward::<T>::mutate_exists(pool, who, |share_info| {
219+
SharesAndWithdrawnRewards::<T>::mutate_exists(pool, who, |share_info| {
134220
if let Some((mut share, mut withdrawn_rewards)) = share_info.take() {
135221
let remove_amount = remove_amount.min(share);
136222

137223
if remove_amount.is_zero() {
138224
return;
139225
}
140226

141-
Pools::<T>::mutate(pool, |pool_info| {
142-
let proportion = FixedU128::checked_from_rational(remove_amount, share)
143-
.expect("share is gte remove_amount and not zero which checked before; qed");
144-
let withdrawn_rewards_to_remove = proportion.saturating_mul_int(withdrawn_rewards);
145-
146-
pool_info.total_shares = pool_info.total_shares.saturating_sub(remove_amount);
147-
pool_info.total_rewards = pool_info.total_rewards.saturating_sub(withdrawn_rewards_to_remove);
148-
pool_info.total_withdrawn_rewards = pool_info
149-
.total_withdrawn_rewards
150-
.saturating_sub(withdrawn_rewards_to_remove);
151-
152-
withdrawn_rewards = withdrawn_rewards.saturating_sub(withdrawn_rewards_to_remove);
227+
PoolInfos::<T>::mutate_exists(pool, |maybe_pool_info| {
228+
if let Some(mut pool_info) = maybe_pool_info.take() {
229+
let removing_share = U256::from(remove_amount.saturated_into::<u128>());
230+
231+
pool_info.total_shares = pool_info.total_shares.saturating_sub(remove_amount);
232+
233+
// update withdrawn rewards for each reward currency
234+
withdrawn_rewards
235+
.iter_mut()
236+
.for_each(|(reward_currency, withdrawn_reward)| {
237+
let withdrawn_reward_to_remove: T::Balance = removing_share
238+
.saturating_mul(withdrawn_reward.to_owned().saturated_into::<u128>().into())
239+
.checked_div(share.saturated_into::<u128>().into())
240+
.unwrap_or_default()
241+
.as_u128()
242+
.saturated_into();
243+
244+
if let Some((total_reward, total_withdrawn_reward)) =
245+
pool_info.rewards.get_mut(reward_currency)
246+
{
247+
*total_reward = total_reward.saturating_sub(withdrawn_reward_to_remove);
248+
*total_withdrawn_reward =
249+
total_withdrawn_reward.saturating_sub(withdrawn_reward_to_remove);
250+
251+
// remove if all reward is withdrawn
252+
if total_reward.is_zero() {
253+
pool_info.rewards.remove(reward_currency);
254+
}
255+
}
256+
*withdrawn_reward = withdrawn_reward.saturating_sub(withdrawn_reward_to_remove);
257+
});
258+
259+
if !pool_info.total_shares.is_zero() {
260+
*maybe_pool_info = Some(pool_info);
261+
}
262+
}
153263
});
154264

155265
share = share.saturating_sub(remove_amount);
@@ -161,7 +271,7 @@ impl<T: Config> Pallet<T> {
161271
}
162272

163273
pub fn set_share(who: &T::AccountId, pool: &T::PoolId, new_share: T::Share) {
164-
let (share, _) = Self::share_and_withdrawn_reward(pool, who);
274+
let (share, _) = Self::shares_and_withdrawn_rewards(pool, who);
165275

166276
if new_share > share {
167277
Self::add_share(who, pool, new_share.saturating_sub(share));
@@ -171,33 +281,44 @@ impl<T: Config> Pallet<T> {
171281
}
172282

173283
pub fn claim_rewards(who: &T::AccountId, pool: &T::PoolId) {
174-
ShareAndWithdrawnReward::<T>::mutate(pool, who, |(share, withdrawn_rewards)| {
175-
if share.is_zero() {
176-
return;
177-
}
178-
179-
Pools::<T>::mutate(pool, |pool_info| {
180-
let proportion = FixedU128::checked_from_rational(*share, pool_info.total_shares).unwrap_or_default();
181-
let reward_to_withdraw = proportion
182-
.saturating_mul_int(pool_info.total_rewards)
183-
.saturating_sub(*withdrawn_rewards)
184-
.min(
185-
pool_info
186-
.total_rewards
187-
.saturating_sub(pool_info.total_withdrawn_rewards),
188-
);
189-
190-
if reward_to_withdraw.is_zero() {
284+
SharesAndWithdrawnRewards::<T>::mutate_exists(pool, who, |maybe_share_withdrawn| {
285+
if let Some((share, withdrawn_rewards)) = maybe_share_withdrawn {
286+
if share.is_zero() {
191287
return;
192288
}
193289

194-
pool_info.total_withdrawn_rewards =
195-
pool_info.total_withdrawn_rewards.saturating_add(reward_to_withdraw);
196-
*withdrawn_rewards = withdrawn_rewards.saturating_add(reward_to_withdraw);
197-
198-
// pay reward to `who`
199-
T::Handler::payout(who, pool, reward_to_withdraw);
200-
});
290+
PoolInfos::<T>::mutate(pool, |pool_info| {
291+
let total_shares = U256::from(pool_info.total_shares.to_owned().saturated_into::<u128>());
292+
pool_info.rewards.iter_mut().for_each(
293+
|(reward_currency, (total_reward, total_withdrawn_reward))| {
294+
let withdrawn_reward = withdrawn_rewards.get(reward_currency).copied().unwrap_or_default();
295+
296+
let total_reward_proportion: T::Balance =
297+
U256::from(share.to_owned().saturated_into::<u128>())
298+
.saturating_mul(U256::from(total_reward.to_owned().saturated_into::<u128>()))
299+
.checked_div(total_shares)
300+
.unwrap_or_default()
301+
.as_u128()
302+
.unique_saturated_into();
303+
304+
let reward_to_withdraw = total_reward_proportion
305+
.saturating_sub(withdrawn_reward)
306+
.min(total_reward.saturating_sub(*total_withdrawn_reward));
307+
308+
if reward_to_withdraw.is_zero() {
309+
return;
310+
}
311+
312+
*total_withdrawn_reward = total_withdrawn_reward.saturating_add(reward_to_withdraw);
313+
withdrawn_rewards
314+
.insert(*reward_currency, withdrawn_reward.saturating_add(reward_to_withdraw));
315+
316+
// pay reward to `who`
317+
T::Handler::payout(who, pool, *reward_currency, reward_to_withdraw);
318+
},
319+
);
320+
});
321+
}
201322
});
202323
}
203324
}

0 commit comments

Comments
 (0)