1#![deny(missing_docs)]
81#![cfg_attr(not(feature = "std"), no_std)]
82
83pub use pallet::*;
84
85use codec::{Codec, Decode, Encode, MaxEncodedLen};
86use frame_support::{
87 traits::{
88 fungibles::{Inspect, Mutate},
89 schedule::DispatchTime,
90 tokens::Balance,
91 },
92 PalletId,
93};
94use frame_system::pallet_prelude::BlockNumberFor;
95use scale_info::TypeInfo;
96use sp_core::Get;
97use sp_runtime::{
98 traits::{MaybeDisplay, Zero},
99 DispatchError,
100};
101use sp_std::boxed::Box;
102
103#[cfg(feature = "runtime-benchmarks")]
104pub mod benchmarking;
105#[cfg(test)]
106mod mock;
107#[cfg(test)]
108mod tests;
109mod weights;
110
111pub use weights::WeightInfo;
112
113pub type PoolId = u32;
115
116pub(crate) const PRECISION_SCALING_FACTOR: u16 = 4096;
118
119pub type PoolInfoFor<T> = PoolInfo<
121 <T as frame_system::Config>::AccountId,
122 <T as Config>::AssetId,
123 <T as Config>::Balance,
124 BlockNumberFor<T>,
125>;
126
127#[derive(Debug, Default, Clone, Decode, Encode, MaxEncodedLen, TypeInfo)]
129pub struct PoolStakerInfo<Balance> {
130 amount: Balance,
132 rewards: Balance,
134 reward_per_token_paid: Balance,
136}
137
138#[derive(Debug, Clone, Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)]
140pub struct PoolInfo<AccountId, AssetId, Balance, BlockNumber> {
141 staked_asset_id: AssetId,
143 reward_asset_id: AssetId,
145 reward_rate_per_block: Balance,
147 expiry_block: BlockNumber,
149 admin: AccountId,
151 total_tokens_staked: Balance,
153 reward_per_token_stored: Balance,
155 last_update_block: BlockNumber,
157 account: AccountId,
159}
160
161sp_api::decl_runtime_apis! {
162 pub trait AssetRewards<Cost: MaybeDisplay + Codec> {
164 fn pool_creation_cost() -> Cost;
168 }
169}
170
171#[frame_support::pallet]
172pub mod pallet {
173 use super::*;
174 use frame_support::{
175 pallet_prelude::*,
176 traits::{
177 fungibles::MutateFreeze,
178 tokens::{AssetId, Fortitude, Preservation},
179 Consideration, Footprint,
180 },
181 };
182 use frame_system::pallet_prelude::*;
183 use sp_runtime::{
184 traits::{
185 AccountIdConversion, BadOrigin, EnsureAdd, EnsureAddAssign, EnsureDiv, EnsureMul,
186 EnsureSub, EnsureSubAssign,
187 },
188 DispatchResult,
189 };
190
191 #[pallet::pallet]
192 pub struct Pallet<T>(_);
193
194 #[pallet::composite_enum]
196 pub enum FreezeReason {
197 #[codec(index = 0)]
199 Staked,
200 }
201
202 #[pallet::composite_enum]
204 pub enum HoldReason {
205 #[codec(index = 0)]
207 PoolCreation,
208 }
209
210 #[pallet::config]
211 pub trait Config: frame_system::Config {
212 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
214
215 #[pallet::constant]
219 type PalletId: Get<PalletId>;
220
221 type AssetId: AssetId + Member + Parameter;
223
224 type Balance: Balance + TypeInfo;
226
227 type CreatePoolOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::AccountId>;
231
232 type Assets: Inspect<Self::AccountId, AssetId = Self::AssetId, Balance = Self::Balance>
235 + Mutate<Self::AccountId>;
236
237 type AssetsFreezer: MutateFreeze<
239 Self::AccountId,
240 Id = Self::RuntimeFreezeReason,
241 AssetId = Self::AssetId,
242 Balance = Self::Balance,
243 >;
244
245 type RuntimeFreezeReason: From<FreezeReason>;
247
248 type Consideration: Consideration<Self::AccountId, Footprint>;
254
255 type WeightInfo: WeightInfo;
257
258 #[cfg(feature = "runtime-benchmarks")]
260 type BenchmarkHelper: benchmarking::BenchmarkHelper<Self::AssetId>;
261 }
262
263 #[pallet::storage]
265 pub type PoolStakers<T: Config> = StorageDoubleMap<
266 _,
267 Blake2_128Concat,
268 PoolId,
269 Blake2_128Concat,
270 T::AccountId,
271 PoolStakerInfo<T::Balance>,
272 >;
273
274 #[pallet::storage]
276 pub type Pools<T: Config> = StorageMap<_, Blake2_128Concat, PoolId, PoolInfoFor<T>>;
277
278 #[pallet::storage]
283 pub type PoolCost<T: Config> =
284 StorageMap<_, Blake2_128Concat, PoolId, (T::AccountId, T::Consideration)>;
285
286 #[pallet::storage]
290 pub type NextPoolId<T: Config> = StorageValue<_, PoolId, ValueQuery>;
291
292 #[pallet::event]
293 #[pallet::generate_deposit(pub(super) fn deposit_event)]
294 pub enum Event<T: Config> {
295 Staked {
297 staker: T::AccountId,
299 pool_id: PoolId,
301 amount: T::Balance,
303 },
304 Unstaked {
306 caller: T::AccountId,
308 staker: T::AccountId,
310 pool_id: PoolId,
312 amount: T::Balance,
314 },
315 RewardsHarvested {
317 caller: T::AccountId,
319 staker: T::AccountId,
321 pool_id: PoolId,
323 amount: T::Balance,
325 },
326 PoolCreated {
328 creator: T::AccountId,
330 pool_id: PoolId,
332 staked_asset_id: T::AssetId,
334 reward_asset_id: T::AssetId,
336 reward_rate_per_block: T::Balance,
338 expiry_block: BlockNumberFor<T>,
340 admin: T::AccountId,
342 },
343 PoolRewardRateModified {
345 pool_id: PoolId,
347 new_reward_rate_per_block: T::Balance,
349 },
350 PoolAdminModified {
352 pool_id: PoolId,
354 new_admin: T::AccountId,
356 },
357 PoolExpiryBlockModified {
359 pool_id: PoolId,
361 new_expiry_block: BlockNumberFor<T>,
363 },
364 PoolCleanedUp {
366 pool_id: PoolId,
368 },
369 }
370
371 #[pallet::error]
372 pub enum Error<T> {
373 NotEnoughTokens,
375 NonExistentPool,
377 NonExistentStaker,
379 NonExistentAsset,
381 BlockNumberConversionError,
383 ExpiryBlockMustBeInTheFuture,
385 InsufficientFunds,
387 ExpiryCut,
389 RewardRateCut,
391 NonEmptyPool,
393 }
394
395 #[pallet::hooks]
396 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
397 fn integrity_test() {
398 let pool_id: PoolId = 1;
400 assert!(
401 <frame_support::PalletId as AccountIdConversion<T::AccountId>>::try_into_sub_account(
402 &T::PalletId::get(), pool_id,
403 )
404 .is_some()
405 );
406 }
407 }
408
409 #[pallet::call(weight(<T as Config>::WeightInfo))]
411 impl<T: Config> Pallet<T> {
412 #[pallet::call_index(0)]
425 pub fn create_pool(
426 origin: OriginFor<T>,
427 staked_asset_id: Box<T::AssetId>,
428 reward_asset_id: Box<T::AssetId>,
429 reward_rate_per_block: T::Balance,
430 expiry: DispatchTime<BlockNumberFor<T>>,
431 admin: Option<T::AccountId>,
432 ) -> DispatchResult {
433 let creator = T::CreatePoolOrigin::ensure_origin(origin)?;
435
436 ensure!(
438 T::Assets::asset_exists(*staked_asset_id.clone()),
439 Error::<T>::NonExistentAsset
440 );
441 ensure!(
442 T::Assets::asset_exists(*reward_asset_id.clone()),
443 Error::<T>::NonExistentAsset
444 );
445
446 let expiry_block = expiry.evaluate(frame_system::Pallet::<T>::block_number());
448 ensure!(
449 expiry_block > frame_system::Pallet::<T>::block_number(),
450 Error::<T>::ExpiryBlockMustBeInTheFuture
451 );
452
453 let pool_id = NextPoolId::<T>::get();
454
455 let footprint = Self::pool_creation_footprint();
456 let cost = T::Consideration::new(&creator, footprint)?;
457 PoolCost::<T>::insert(pool_id, (creator.clone(), cost));
458
459 let admin = admin.unwrap_or(creator.clone());
460
461 let pool = PoolInfoFor::<T> {
463 staked_asset_id: *staked_asset_id.clone(),
464 reward_asset_id: *reward_asset_id.clone(),
465 reward_rate_per_block,
466 total_tokens_staked: 0u32.into(),
467 reward_per_token_stored: 0u32.into(),
468 last_update_block: 0u32.into(),
469 expiry_block,
470 admin: admin.clone(),
471 account: Self::pool_account_id(&pool_id),
472 };
473
474 Pools::<T>::insert(pool_id, pool);
476
477 NextPoolId::<T>::put(pool_id.ensure_add(1)?);
478
479 Self::deposit_event(Event::PoolCreated {
481 creator,
482 pool_id,
483 staked_asset_id: *staked_asset_id,
484 reward_asset_id: *reward_asset_id,
485 reward_rate_per_block,
486 expiry_block,
487 admin,
488 });
489
490 Ok(())
491 }
492
493 #[pallet::call_index(1)]
497 pub fn stake(origin: OriginFor<T>, pool_id: PoolId, amount: T::Balance) -> DispatchResult {
498 let staker = ensure_signed(origin)?;
499
500 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
502 let staker_info = PoolStakers::<T>::get(pool_id, &staker).unwrap_or_default();
503 let (mut pool_info, mut staker_info) =
504 Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
505
506 T::AssetsFreezer::increase_frozen(
507 pool_info.staked_asset_id.clone(),
508 &FreezeReason::Staked.into(),
509 &staker,
510 amount,
511 )?;
512
513 pool_info.total_tokens_staked.ensure_add_assign(amount)?;
515
516 Pools::<T>::insert(pool_id, pool_info);
517
518 staker_info.amount.ensure_add_assign(amount)?;
520 PoolStakers::<T>::insert(pool_id, &staker, staker_info);
521
522 Self::deposit_event(Event::Staked { staker, pool_id, amount });
524
525 Ok(())
526 }
527
528 #[pallet::call_index(2)]
538 pub fn unstake(
539 origin: OriginFor<T>,
540 pool_id: PoolId,
541 amount: T::Balance,
542 staker: Option<T::AccountId>,
543 ) -> DispatchResult {
544 let caller = ensure_signed(origin)?;
545 let staker = staker.unwrap_or(caller.clone());
546
547 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
549 let now = frame_system::Pallet::<T>::block_number();
550 ensure!(now > pool_info.expiry_block || caller == staker, BadOrigin);
551
552 let staker_info = PoolStakers::<T>::get(pool_id, &staker).unwrap_or_default();
553 let (mut pool_info, mut staker_info) =
554 Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
555
556 ensure!(staker_info.amount >= amount, Error::<T>::NotEnoughTokens);
558
559 T::AssetsFreezer::decrease_frozen(
561 pool_info.staked_asset_id.clone(),
562 &FreezeReason::Staked.into(),
563 &staker,
564 amount,
565 )?;
566
567 pool_info.total_tokens_staked.ensure_sub_assign(amount)?;
569 Pools::<T>::insert(pool_id, pool_info);
570
571 staker_info.amount.ensure_sub_assign(amount)?;
573
574 if staker_info.amount.is_zero() && staker_info.rewards.is_zero() {
575 PoolStakers::<T>::remove(&pool_id, &staker);
576 } else {
577 PoolStakers::<T>::insert(&pool_id, &staker, staker_info);
578 }
579
580 Self::deposit_event(Event::Unstaked { caller, staker, pool_id, amount });
582
583 Ok(())
584 }
585
586 #[pallet::call_index(3)]
593 pub fn harvest_rewards(
594 origin: OriginFor<T>,
595 pool_id: PoolId,
596 staker: Option<T::AccountId>,
597 ) -> DispatchResult {
598 let caller = ensure_signed(origin)?;
599 let staker = staker.unwrap_or(caller.clone());
600
601 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
603 let now = frame_system::Pallet::<T>::block_number();
604 ensure!(now > pool_info.expiry_block || caller == staker, BadOrigin);
605
606 let staker_info =
607 PoolStakers::<T>::get(pool_id, &staker).ok_or(Error::<T>::NonExistentStaker)?;
608 let (pool_info, mut staker_info) =
609 Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
610
611 T::Assets::transfer(
613 pool_info.reward_asset_id,
614 &pool_info.account,
615 &staker,
616 staker_info.rewards,
617 Preservation::Expendable,
619 )?;
620
621 Self::deposit_event(Event::RewardsHarvested {
623 caller,
624 staker: staker.clone(),
625 pool_id,
626 amount: staker_info.rewards,
627 });
628
629 staker_info.rewards = 0u32.into();
631
632 if staker_info.amount.is_zero() {
633 PoolStakers::<T>::remove(&pool_id, &staker);
634 } else {
635 PoolStakers::<T>::insert(&pool_id, &staker, staker_info);
636 }
637
638 Ok(())
639 }
640
641 #[pallet::call_index(4)]
647 pub fn set_pool_reward_rate_per_block(
648 origin: OriginFor<T>,
649 pool_id: PoolId,
650 new_reward_rate_per_block: T::Balance,
651 ) -> DispatchResult {
652 let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
653 .or_else(|_| ensure_signed(origin))?;
654
655 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
656 ensure!(pool_info.admin == caller, BadOrigin);
657 ensure!(
658 new_reward_rate_per_block > pool_info.reward_rate_per_block,
659 Error::<T>::RewardRateCut
660 );
661
662 let rewards_per_token = Self::reward_per_token(&pool_info)?;
664 let mut pool_info = Self::update_pool_rewards(&pool_info, rewards_per_token)?;
665
666 pool_info.reward_rate_per_block = new_reward_rate_per_block;
667 Pools::<T>::insert(pool_id, pool_info);
668
669 Self::deposit_event(Event::PoolRewardRateModified {
670 pool_id,
671 new_reward_rate_per_block,
672 });
673
674 Ok(())
675 }
676
677 #[pallet::call_index(5)]
681 pub fn set_pool_admin(
682 origin: OriginFor<T>,
683 pool_id: PoolId,
684 new_admin: T::AccountId,
685 ) -> DispatchResult {
686 let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
687 .or_else(|_| ensure_signed(origin))?;
688
689 let mut pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
690 ensure!(pool_info.admin == caller, BadOrigin);
691
692 pool_info.admin = new_admin.clone();
693 Pools::<T>::insert(pool_id, pool_info);
694
695 Self::deposit_event(Event::PoolAdminModified { pool_id, new_admin });
696
697 Ok(())
698 }
699
700 #[pallet::call_index(6)]
706 pub fn set_pool_expiry_block(
707 origin: OriginFor<T>,
708 pool_id: PoolId,
709 new_expiry: DispatchTime<BlockNumberFor<T>>,
710 ) -> DispatchResult {
711 let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
712 .or_else(|_| ensure_signed(origin))?;
713
714 let new_expiry = new_expiry.evaluate(frame_system::Pallet::<T>::block_number());
715 ensure!(
716 new_expiry > frame_system::Pallet::<T>::block_number(),
717 Error::<T>::ExpiryBlockMustBeInTheFuture
718 );
719
720 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
721 ensure!(pool_info.admin == caller, BadOrigin);
722 ensure!(new_expiry > pool_info.expiry_block, Error::<T>::ExpiryCut);
723
724 let reward_per_token = Self::reward_per_token(&pool_info)?;
726 let mut pool_info = Self::update_pool_rewards(&pool_info, reward_per_token)?;
727
728 pool_info.expiry_block = new_expiry;
729 Pools::<T>::insert(pool_id, pool_info);
730
731 Self::deposit_event(Event::PoolExpiryBlockModified {
732 pool_id,
733 new_expiry_block: new_expiry,
734 });
735
736 Ok(())
737 }
738
739 #[pallet::call_index(7)]
745 pub fn deposit_reward_tokens(
746 origin: OriginFor<T>,
747 pool_id: PoolId,
748 amount: T::Balance,
749 ) -> DispatchResult {
750 let caller = ensure_signed(origin)?;
751 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
752 T::Assets::transfer(
753 pool_info.reward_asset_id,
754 &caller,
755 &pool_info.account,
756 amount,
757 Preservation::Preserve,
758 )?;
759 Ok(())
760 }
761
762 #[pallet::call_index(8)]
769 pub fn cleanup_pool(origin: OriginFor<T>, pool_id: PoolId) -> DispatchResult {
770 let who = ensure_signed(origin)?;
771
772 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
773 ensure!(pool_info.admin == who, BadOrigin);
774
775 let stakers = PoolStakers::<T>::iter_key_prefix(pool_id).next();
776 ensure!(stakers.is_none(), Error::<T>::NonEmptyPool);
777
778 let pool_balance = T::Assets::reducible_balance(
779 pool_info.reward_asset_id.clone(),
780 &pool_info.account,
781 Preservation::Expendable,
782 Fortitude::Polite,
783 );
784 T::Assets::transfer(
785 pool_info.reward_asset_id,
786 &pool_info.account,
787 &pool_info.admin,
788 pool_balance,
789 Preservation::Expendable,
790 )?;
791
792 if let Some((who, cost)) = PoolCost::<T>::take(pool_id) {
793 T::Consideration::drop(cost, &who)?;
794 }
795
796 Pools::<T>::remove(pool_id);
797
798 Self::deposit_event(Event::PoolCleanedUp { pool_id });
799
800 Ok(())
801 }
802 }
803
804 impl<T: Config> Pallet<T> {
805 pub fn pool_creation_footprint() -> Footprint {
810 Footprint::from_mel::<(PoolId, PoolInfoFor<T>)>()
811 }
812
813 pub fn pool_account_id(id: &PoolId) -> T::AccountId {
815 T::PalletId::get().into_sub_account_truncating(id)
816 }
817
818 pub fn update_pool_and_staker_rewards(
827 pool_info: &PoolInfoFor<T>,
828 staker_info: &PoolStakerInfo<T::Balance>,
829 ) -> Result<(PoolInfoFor<T>, PoolStakerInfo<T::Balance>), DispatchError> {
830 let reward_per_token = Self::reward_per_token(&pool_info)?;
831 let pool_info = Self::update_pool_rewards(pool_info, reward_per_token)?;
832
833 let mut new_staker_info = staker_info.clone();
834 new_staker_info.rewards = Self::derive_rewards(&staker_info, &reward_per_token)?;
835 new_staker_info.reward_per_token_paid = pool_info.reward_per_token_stored;
836 return Ok((pool_info, new_staker_info));
837 }
838
839 pub fn update_pool_rewards(
848 pool_info: &PoolInfoFor<T>,
849 reward_per_token: T::Balance,
850 ) -> Result<PoolInfoFor<T>, DispatchError> {
851 let mut new_pool_info = pool_info.clone();
852 new_pool_info.last_update_block = frame_system::Pallet::<T>::block_number();
853 new_pool_info.reward_per_token_stored = reward_per_token;
854
855 Ok(new_pool_info)
856 }
857
858 fn reward_per_token(pool_info: &PoolInfoFor<T>) -> Result<T::Balance, DispatchError> {
860 if pool_info.total_tokens_staked.is_zero() {
861 return Ok(pool_info.reward_per_token_stored)
862 }
863
864 let rewardable_blocks_elapsed: u32 =
865 match Self::last_block_reward_applicable(pool_info.expiry_block)
866 .ensure_sub(pool_info.last_update_block)?
867 .try_into()
868 {
869 Ok(b) => b,
870 Err(_) => return Err(Error::<T>::BlockNumberConversionError.into()),
871 };
872
873 Ok(pool_info.reward_per_token_stored.ensure_add(
874 pool_info
875 .reward_rate_per_block
876 .ensure_mul(rewardable_blocks_elapsed.into())?
877 .ensure_mul(PRECISION_SCALING_FACTOR.into())?
878 .ensure_div(pool_info.total_tokens_staked)?,
879 )?)
880 }
881
882 fn derive_rewards(
886 staker_info: &PoolStakerInfo<T::Balance>,
887 reward_per_token: &T::Balance,
888 ) -> Result<T::Balance, DispatchError> {
889 Ok(staker_info
890 .amount
891 .ensure_mul(reward_per_token.ensure_sub(staker_info.reward_per_token_paid)?)?
892 .ensure_div(PRECISION_SCALING_FACTOR.into())?
893 .ensure_add(staker_info.rewards)?)
894 }
895
896 fn last_block_reward_applicable(pool_expiry_block: BlockNumberFor<T>) -> BlockNumberFor<T> {
897 let now = frame_system::Pallet::<T>::block_number();
898 if now < pool_expiry_block {
899 now
900 } else {
901 pool_expiry_block
902 }
903 }
904 }
905}