orml_rewards/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![allow(clippy::unused_unit)]
3#![allow(clippy::too_many_arguments)]
4
5mod mock;
6mod tests;
7
8use frame_support::pallet_prelude::*;
9use orml_traits::{GetByKey, RewardHandler};
10use parity_scale_codec::{FullCodec, HasCompact};
11use scale_info::TypeInfo;
12use sp_core::U256;
13use sp_runtime::{
14	traits::{AtLeast32BitUnsigned, MaybeSerializeDeserialize, Member, Saturating, UniqueSaturatedInto, Zero},
15	FixedPointOperand, RuntimeDebug, SaturatedConversion,
16};
17use sp_std::{borrow::ToOwned, collections::btree_map::BTreeMap, fmt::Debug, prelude::*};
18
19/// The Reward Pool Info.
20#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, RuntimeDebug, TypeInfo)]
21pub struct PoolInfo<Share, Balance, CurrencyId>
22where
23	Share: HasCompact,
24	Balance: HasCompact,
25	CurrencyId: Ord,
26{
27	/// Total shares amount
28	pub total_shares: Share,
29	/// Reward infos <reward_currency, (total_reward, total_withdrawn_reward)>
30	pub rewards: BTreeMap<CurrencyId, (Balance, Balance)>,
31}
32
33impl<Share, Balance, CurrencyId> Default for PoolInfo<Share, Balance, CurrencyId>
34where
35	Share: Default + HasCompact,
36	Balance: HasCompact,
37	CurrencyId: Ord,
38{
39	fn default() -> Self {
40		Self {
41			total_shares: Default::default(),
42			rewards: BTreeMap::new(),
43		}
44	}
45}
46
47pub use module::*;
48
49#[frame_support::pallet]
50pub mod module {
51
52	use super::*;
53
54	#[pallet::config]
55	pub trait Config: frame_system::Config {
56		/// The share type of pool.
57		type Share: Parameter
58			+ Member
59			+ AtLeast32BitUnsigned
60			+ Default
61			+ Copy
62			+ MaybeSerializeDeserialize
63			+ Debug
64			+ FixedPointOperand;
65
66		/// The reward balance type.
67		type Balance: Parameter
68			+ Member
69			+ AtLeast32BitUnsigned
70			+ Default
71			+ Copy
72			+ MaybeSerializeDeserialize
73			+ Debug
74			+ FixedPointOperand;
75
76		/// The reward pool ID type.
77		type PoolId: Parameter + Member + Clone + FullCodec;
78
79		type CurrencyId: Parameter + Member + Copy + MaybeSerializeDeserialize + Ord;
80
81		/// The minimal amount of shares an account can hold.
82		/// Transactions that would result in an account holding shares fewer
83		/// than this amount but non zero are invalid.
84		type MinimalShares: GetByKey<Self::PoolId, Self::Share>;
85
86		/// The `RewardHandler`
87		type Handler: RewardHandler<Self::AccountId, Self::CurrencyId, Balance = Self::Balance, PoolId = Self::PoolId>;
88	}
89
90	type WithdrawnRewards<T> = BTreeMap<<T as Config>::CurrencyId, <T as Config>::Balance>;
91
92	#[pallet::error]
93	pub enum Error<T> {
94		/// Pool does not exist
95		PoolDoesNotExist,
96		/// Account does not have share
97		ShareDoesNotExist,
98		/// Can split only less than share
99		CanSplitOnlyLessThanShare,
100		/// Share amount below minimal
101		ShareBelowMinimal,
102	}
103
104	/// Record reward pool info.
105	///
106	/// map PoolId => PoolInfo
107	#[pallet::storage]
108	#[pallet::getter(fn pool_infos)]
109	pub type PoolInfos<T: Config> =
110		StorageMap<_, Twox64Concat, T::PoolId, PoolInfo<T::Share, T::Balance, T::CurrencyId>, ValueQuery>;
111
112	/// Record share amount, reward currency and withdrawn reward amount for
113	/// specific `AccountId` under `PoolId`.
114	///
115	/// double_map (PoolId, AccountId) => (Share, BTreeMap<CurrencyId, Balance>)
116	#[pallet::storage]
117	#[pallet::getter(fn shares_and_withdrawn_rewards)]
118	pub type SharesAndWithdrawnRewards<T: Config> = StorageDoubleMap<
119		_,
120		Twox64Concat,
121		T::PoolId,
122		Twox64Concat,
123		T::AccountId,
124		(T::Share, WithdrawnRewards<T>),
125		ValueQuery,
126	>;
127
128	#[pallet::pallet]
129	#[pallet::without_storage_info]
130	pub struct Pallet<T>(_);
131}
132
133impl<T: Config> Pallet<T> {
134	pub fn accumulate_reward(
135		pool: &T::PoolId,
136		reward_currency: T::CurrencyId,
137		reward_increment: T::Balance,
138	) -> DispatchResult {
139		if reward_increment.is_zero() {
140			return Ok(());
141		}
142		PoolInfos::<T>::mutate_exists(pool, |maybe_pool_info| -> DispatchResult {
143			let pool_info = maybe_pool_info.as_mut().ok_or(Error::<T>::PoolDoesNotExist)?;
144
145			pool_info
146				.rewards
147				.entry(reward_currency)
148				.and_modify(|(total_reward, _)| {
149					*total_reward = total_reward.saturating_add(reward_increment);
150				})
151				.or_insert((reward_increment, Zero::zero()));
152
153			Ok(())
154		})
155	}
156
157	pub fn add_share(who: &T::AccountId, pool: &T::PoolId, add_amount: T::Share) -> DispatchResult {
158		if add_amount.is_zero() {
159			return Ok(());
160		}
161
162		PoolInfos::<T>::try_mutate(pool, |pool_info| {
163			let initial_total_shares = pool_info.total_shares;
164			pool_info.total_shares = pool_info.total_shares.saturating_add(add_amount);
165
166			let mut withdrawn_inflation = Vec::<(T::CurrencyId, T::Balance)>::new();
167
168			pool_info
169				.rewards
170				.iter_mut()
171				.for_each(|(reward_currency, (total_reward, total_withdrawn_reward))| {
172					let reward_inflation = if initial_total_shares.is_zero() {
173						Zero::zero()
174					} else {
175						U256::from(add_amount.to_owned().saturated_into::<u128>())
176							.saturating_mul(total_reward.to_owned().saturated_into::<u128>().into())
177							.checked_div(initial_total_shares.to_owned().saturated_into::<u128>().into())
178							.unwrap_or_default()
179							.saturated_into::<u128>()
180							.saturated_into()
181					};
182					*total_reward = total_reward.saturating_add(reward_inflation);
183					*total_withdrawn_reward = total_withdrawn_reward.saturating_add(reward_inflation);
184
185					withdrawn_inflation.push((*reward_currency, reward_inflation));
186				});
187
188			SharesAndWithdrawnRewards::<T>::try_mutate(pool, who, |(share, withdrawn_rewards)| {
189				*share = share.saturating_add(add_amount);
190
191				ensure!(*share >= T::MinimalShares::get(pool), Error::<T>::ShareBelowMinimal);
192
193				// update withdrawn inflation for each reward currency
194				withdrawn_inflation
195					.into_iter()
196					.for_each(|(reward_currency, reward_inflation)| {
197						withdrawn_rewards
198							.entry(reward_currency)
199							.and_modify(|withdrawn_reward| {
200								*withdrawn_reward = withdrawn_reward.saturating_add(reward_inflation);
201							})
202							.or_insert(reward_inflation);
203					});
204
205				Ok(())
206			})
207		})
208	}
209
210	pub fn remove_share(who: &T::AccountId, pool: &T::PoolId, remove_amount: T::Share) -> DispatchResult {
211		if remove_amount.is_zero() {
212			return Ok(());
213		}
214
215		// claim rewards firstly
216		Self::claim_rewards(who, pool);
217
218		SharesAndWithdrawnRewards::<T>::try_mutate_exists(pool, who, |share_info| {
219			if let Some((mut share, mut withdrawn_rewards)) = share_info.take() {
220				let remove_amount = remove_amount.min(share);
221
222				if remove_amount.is_zero() {
223					return Ok(());
224				}
225
226				let old_share = share;
227
228				share = share.saturating_sub(remove_amount);
229				if !share.is_zero() {
230					ensure!(share >= T::MinimalShares::get(pool), Error::<T>::ShareBelowMinimal);
231				}
232
233				PoolInfos::<T>::try_mutate_exists(pool, |maybe_pool_info| -> DispatchResult {
234					if let Some(mut pool_info) = maybe_pool_info.take() {
235						let removing_share = U256::from(remove_amount.saturated_into::<u128>());
236
237						pool_info.total_shares = pool_info.total_shares.saturating_sub(remove_amount);
238
239						// update withdrawn rewards for each reward currency
240						withdrawn_rewards
241							.iter_mut()
242							.for_each(|(reward_currency, withdrawn_reward)| {
243								let withdrawn_reward_to_remove: T::Balance = removing_share
244									.saturating_mul(withdrawn_reward.to_owned().saturated_into::<u128>().into())
245									.checked_div(old_share.saturated_into::<u128>().into())
246									.unwrap_or_default()
247									.saturated_into::<u128>()
248									.saturated_into();
249
250								if let Some((total_reward, total_withdrawn_reward)) =
251									pool_info.rewards.get_mut(reward_currency)
252								{
253									*total_reward = total_reward.saturating_sub(withdrawn_reward_to_remove);
254									*total_withdrawn_reward =
255										total_withdrawn_reward.saturating_sub(withdrawn_reward_to_remove);
256
257									// remove if all reward is withdrawn
258									if total_reward.is_zero() {
259										pool_info.rewards.remove(reward_currency);
260									}
261								}
262								*withdrawn_reward = withdrawn_reward.saturating_sub(withdrawn_reward_to_remove);
263							});
264
265						if !pool_info.total_shares.is_zero() {
266							*maybe_pool_info = Some(pool_info);
267						}
268					}
269
270					if !share.is_zero() {
271						*share_info = Some((share, withdrawn_rewards));
272					}
273
274					Ok(())
275				})?;
276			}
277
278			Ok(())
279		})
280	}
281
282	pub fn set_share(who: &T::AccountId, pool: &T::PoolId, new_share: T::Share) -> DispatchResult {
283		let (share, _) = Self::shares_and_withdrawn_rewards(pool, who);
284
285		if new_share > share {
286			Self::add_share(who, pool, new_share.saturating_sub(share))
287		} else {
288			Self::remove_share(who, pool, share.saturating_sub(new_share))
289		}
290	}
291
292	pub fn claim_rewards(who: &T::AccountId, pool: &T::PoolId) {
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_exists(pool, |maybe_pool_info| {
300					if let Some(pool_info) = maybe_pool_info {
301						let total_shares = U256::from(pool_info.total_shares.to_owned().saturated_into::<u128>());
302						pool_info.rewards.iter_mut().for_each(
303							|(reward_currency, (total_reward, total_withdrawn_reward))| {
304								Self::claim_one(
305									withdrawn_rewards,
306									*reward_currency,
307									share.to_owned(),
308									total_reward.to_owned(),
309									total_shares,
310									total_withdrawn_reward,
311									who,
312									pool,
313								);
314							},
315						);
316					}
317				});
318			}
319		});
320	}
321
322	pub fn claim_reward(who: &T::AccountId, pool: &T::PoolId, reward_currency: T::CurrencyId) {
323		SharesAndWithdrawnRewards::<T>::mutate_exists(pool, who, |maybe_share_withdrawn| {
324			if let Some((share, withdrawn_rewards)) = maybe_share_withdrawn {
325				if share.is_zero() {
326					return;
327				}
328
329				PoolInfos::<T>::mutate(pool, |pool_info| {
330					let total_shares = U256::from(pool_info.total_shares.to_owned().saturated_into::<u128>());
331					if let Some((total_reward, total_withdrawn_reward)) = pool_info.rewards.get_mut(&reward_currency) {
332						Self::claim_one(
333							withdrawn_rewards,
334							reward_currency,
335							share.to_owned(),
336							total_reward.to_owned(),
337							total_shares,
338							total_withdrawn_reward,
339							who,
340							pool,
341						);
342					}
343				});
344			}
345		});
346	}
347
348	/// Splits share into two parts.
349	///
350	/// `move_share` - amount of share to remove and put into `other` share
351	/// `other` - new account who will own new share
352	///
353	/// Similar too claim and add 2 shares later, but does not requires pool
354	/// inflation and is more efficient.
355	pub fn transfer_share_and_rewards(
356		who: &T::AccountId,
357		pool: &T::PoolId,
358		move_share: T::Share,
359		other: &T::AccountId,
360	) -> DispatchResult {
361		if move_share.is_zero() {
362			return Ok(());
363		}
364
365		SharesAndWithdrawnRewards::<T>::try_mutate(pool, other, |increased_share| {
366			let (increased_share, increased_rewards) = increased_share;
367			SharesAndWithdrawnRewards::<T>::try_mutate_exists(pool, who, |share| {
368				let (share, rewards) = share.as_mut().ok_or(Error::<T>::ShareDoesNotExist)?;
369				ensure!(move_share < *share, Error::<T>::CanSplitOnlyLessThanShare);
370				if who == other {
371					// self transfer is noop
372					return Ok(());
373				}
374				for (reward_currency, balance) in rewards {
375					// u128 * u128 is always less than u256
376					// move_share / share always less then 1 and share > 0
377					// so final results is computable and is always less or equal than u128
378					let move_balance = U256::from(balance.to_owned().saturated_into::<u128>())
379						* U256::from(move_share.to_owned().saturated_into::<u128>())
380						/ U256::from(share.to_owned().saturated_into::<u128>());
381					let move_balance: Option<u128> = move_balance.try_into().ok();
382					if let Some(move_balance) = move_balance {
383						let move_balance: T::Balance = move_balance.unique_saturated_into();
384						*balance = balance.saturating_sub(move_balance);
385						increased_rewards
386							.entry(*reward_currency)
387							.and_modify(|increased_reward| {
388								*increased_reward = increased_reward.saturating_add(move_balance);
389							})
390							.or_insert(move_balance);
391					}
392				}
393				*share = share.saturating_sub(move_share);
394				*increased_share = increased_share.saturating_add(move_share);
395
396				ensure!(
397					*share >= T::MinimalShares::get(pool) || share.is_zero(),
398					Error::<T>::ShareBelowMinimal
399				);
400				ensure!(
401					*increased_share >= T::MinimalShares::get(pool),
402					Error::<T>::ShareBelowMinimal
403				);
404
405				Ok(())
406			})
407		})
408	}
409
410	#[allow(clippy::too_many_arguments)] // just we need to have all these to do the stuff
411	fn claim_one(
412		withdrawn_rewards: &mut BTreeMap<T::CurrencyId, T::Balance>,
413		reward_currency: T::CurrencyId,
414		share: T::Share,
415		total_reward: T::Balance,
416		total_shares: U256,
417		total_withdrawn_reward: &mut T::Balance,
418		who: &T::AccountId,
419		pool: &T::PoolId,
420	) {
421		let withdrawn_reward = withdrawn_rewards.get(&reward_currency).copied().unwrap_or_default();
422		let reward_to_withdraw = Self::reward_to_withdraw(
423			share,
424			total_reward,
425			total_shares,
426			withdrawn_reward,
427			total_withdrawn_reward.to_owned(),
428		);
429		if !reward_to_withdraw.is_zero() {
430			*total_withdrawn_reward = total_withdrawn_reward.saturating_add(reward_to_withdraw);
431			withdrawn_rewards.insert(reward_currency, withdrawn_reward.saturating_add(reward_to_withdraw));
432
433			// pay reward to `who`
434			T::Handler::payout(who, pool, reward_currency, reward_to_withdraw);
435		}
436	}
437
438	fn reward_to_withdraw(
439		share: T::Share,
440		total_reward: T::Balance,
441		total_shares: U256,
442		withdrawn_reward: T::Balance,
443		total_withdrawn_reward: T::Balance,
444	) -> T::Balance {
445		let total_reward_proportion: T::Balance = U256::from(share.saturated_into::<u128>())
446			.saturating_mul(U256::from(total_reward.saturated_into::<u128>()))
447			.checked_div(total_shares)
448			.unwrap_or_default()
449			.as_u128()
450			.unique_saturated_into();
451		total_reward_proportion
452			.saturating_sub(withdrawn_reward)
453			.min(total_reward.saturating_sub(total_withdrawn_reward))
454	}
455}