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#[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 pub total_shares: Share,
29 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 type Share: Parameter
58 + Member
59 + AtLeast32BitUnsigned
60 + Default
61 + Copy
62 + MaybeSerializeDeserialize
63 + Debug
64 + FixedPointOperand;
65
66 type Balance: Parameter
68 + Member
69 + AtLeast32BitUnsigned
70 + Default
71 + Copy
72 + MaybeSerializeDeserialize
73 + Debug
74 + FixedPointOperand;
75
76 type PoolId: Parameter + Member + Clone + FullCodec;
78
79 type CurrencyId: Parameter + Member + Copy + MaybeSerializeDeserialize + Ord;
80
81 type MinimalShares: GetByKey<Self::PoolId, Self::Share>;
85
86 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 PoolDoesNotExist,
96 ShareDoesNotExist,
98 CanSplitOnlyLessThanShare,
100 ShareBelowMinimal,
102 }
103
104 #[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 #[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 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 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 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 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 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 return Ok(());
373 }
374 for (reward_currency, balance) in rewards {
375 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)] 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 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}