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 #[allow(deprecated)]
214 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
215
216 #[pallet::constant]
220 type PalletId: Get<PalletId>;
221
222 type AssetId: AssetId + Member + Parameter;
224
225 type Balance: Balance + TypeInfo;
227
228 type CreatePoolOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::AccountId>;
232
233 type Assets: Inspect<Self::AccountId, AssetId = Self::AssetId, Balance = Self::Balance>
236 + Mutate<Self::AccountId>;
237
238 type AssetsFreezer: MutateFreeze<
240 Self::AccountId,
241 Id = Self::RuntimeFreezeReason,
242 AssetId = Self::AssetId,
243 Balance = Self::Balance,
244 >;
245
246 type RuntimeFreezeReason: From<FreezeReason>;
248
249 type Consideration: Consideration<Self::AccountId, Footprint>;
255
256 type WeightInfo: WeightInfo;
258
259 #[cfg(feature = "runtime-benchmarks")]
261 type BenchmarkHelper: benchmarking::BenchmarkHelper<Self::AssetId>;
262 }
263
264 #[pallet::storage]
266 pub type PoolStakers<T: Config> = StorageDoubleMap<
267 _,
268 Blake2_128Concat,
269 PoolId,
270 Blake2_128Concat,
271 T::AccountId,
272 PoolStakerInfo<T::Balance>,
273 >;
274
275 #[pallet::storage]
277 pub type Pools<T: Config> = StorageMap<_, Blake2_128Concat, PoolId, PoolInfoFor<T>>;
278
279 #[pallet::storage]
284 pub type PoolCost<T: Config> =
285 StorageMap<_, Blake2_128Concat, PoolId, (T::AccountId, T::Consideration)>;
286
287 #[pallet::storage]
291 pub type NextPoolId<T: Config> = StorageValue<_, PoolId, ValueQuery>;
292
293 #[pallet::event]
294 #[pallet::generate_deposit(pub(super) fn deposit_event)]
295 pub enum Event<T: Config> {
296 Staked {
298 staker: T::AccountId,
300 pool_id: PoolId,
302 amount: T::Balance,
304 },
305 Unstaked {
307 caller: T::AccountId,
309 staker: T::AccountId,
311 pool_id: PoolId,
313 amount: T::Balance,
315 },
316 RewardsHarvested {
318 caller: T::AccountId,
320 staker: T::AccountId,
322 pool_id: PoolId,
324 amount: T::Balance,
326 },
327 PoolCreated {
329 creator: T::AccountId,
331 pool_id: PoolId,
333 staked_asset_id: T::AssetId,
335 reward_asset_id: T::AssetId,
337 reward_rate_per_block: T::Balance,
339 expiry_block: BlockNumberFor<T>,
341 admin: T::AccountId,
343 },
344 PoolRewardRateModified {
346 pool_id: PoolId,
348 new_reward_rate_per_block: T::Balance,
350 },
351 PoolAdminModified {
353 pool_id: PoolId,
355 new_admin: T::AccountId,
357 },
358 PoolExpiryBlockModified {
360 pool_id: PoolId,
362 new_expiry_block: BlockNumberFor<T>,
364 },
365 PoolCleanedUp {
367 pool_id: PoolId,
369 },
370 }
371
372 #[pallet::error]
373 pub enum Error<T> {
374 NotEnoughTokens,
376 NonExistentPool,
378 NonExistentStaker,
380 NonExistentAsset,
382 BlockNumberConversionError,
384 ExpiryBlockMustBeInTheFuture,
386 InsufficientFunds,
388 ExpiryCut,
390 RewardRateCut,
392 NonEmptyPool,
394 }
395
396 #[pallet::hooks]
397 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
398 fn integrity_test() {
399 let pool_id: PoolId = 1;
401 assert!(
402 <frame_support::PalletId as AccountIdConversion<T::AccountId>>::try_into_sub_account(
403 &T::PalletId::get(), pool_id,
404 )
405 .is_some()
406 );
407 }
408 }
409
410 #[pallet::call(weight(<T as Config>::WeightInfo))]
412 impl<T: Config> Pallet<T> {
413 #[pallet::call_index(0)]
426 pub fn create_pool(
427 origin: OriginFor<T>,
428 staked_asset_id: Box<T::AssetId>,
429 reward_asset_id: Box<T::AssetId>,
430 reward_rate_per_block: T::Balance,
431 expiry: DispatchTime<BlockNumberFor<T>>,
432 admin: Option<T::AccountId>,
433 ) -> DispatchResult {
434 let creator = T::CreatePoolOrigin::ensure_origin(origin)?;
436
437 ensure!(
439 T::Assets::asset_exists(*staked_asset_id.clone()),
440 Error::<T>::NonExistentAsset
441 );
442 ensure!(
443 T::Assets::asset_exists(*reward_asset_id.clone()),
444 Error::<T>::NonExistentAsset
445 );
446
447 let expiry_block = expiry.evaluate(frame_system::Pallet::<T>::block_number());
449 ensure!(
450 expiry_block > frame_system::Pallet::<T>::block_number(),
451 Error::<T>::ExpiryBlockMustBeInTheFuture
452 );
453
454 let pool_id = NextPoolId::<T>::get();
455
456 let footprint = Self::pool_creation_footprint();
457 let cost = T::Consideration::new(&creator, footprint)?;
458 PoolCost::<T>::insert(pool_id, (creator.clone(), cost));
459
460 let admin = admin.unwrap_or(creator.clone());
461
462 let pool = PoolInfoFor::<T> {
464 staked_asset_id: *staked_asset_id.clone(),
465 reward_asset_id: *reward_asset_id.clone(),
466 reward_rate_per_block,
467 total_tokens_staked: 0u32.into(),
468 reward_per_token_stored: 0u32.into(),
469 last_update_block: 0u32.into(),
470 expiry_block,
471 admin: admin.clone(),
472 account: Self::pool_account_id(&pool_id),
473 };
474
475 Pools::<T>::insert(pool_id, pool);
477
478 NextPoolId::<T>::put(pool_id.ensure_add(1)?);
479
480 Self::deposit_event(Event::PoolCreated {
482 creator,
483 pool_id,
484 staked_asset_id: *staked_asset_id,
485 reward_asset_id: *reward_asset_id,
486 reward_rate_per_block,
487 expiry_block,
488 admin,
489 });
490
491 Ok(())
492 }
493
494 #[pallet::call_index(1)]
498 pub fn stake(origin: OriginFor<T>, pool_id: PoolId, amount: T::Balance) -> DispatchResult {
499 let staker = ensure_signed(origin)?;
500
501 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
503 let staker_info = PoolStakers::<T>::get(pool_id, &staker).unwrap_or_default();
504 let (mut pool_info, mut staker_info) =
505 Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
506
507 T::AssetsFreezer::increase_frozen(
508 pool_info.staked_asset_id.clone(),
509 &FreezeReason::Staked.into(),
510 &staker,
511 amount,
512 )?;
513
514 pool_info.total_tokens_staked.ensure_add_assign(amount)?;
516
517 Pools::<T>::insert(pool_id, pool_info);
518
519 staker_info.amount.ensure_add_assign(amount)?;
521 PoolStakers::<T>::insert(pool_id, &staker, staker_info);
522
523 Self::deposit_event(Event::Staked { staker, pool_id, amount });
525
526 Ok(())
527 }
528
529 #[pallet::call_index(2)]
539 pub fn unstake(
540 origin: OriginFor<T>,
541 pool_id: PoolId,
542 amount: T::Balance,
543 staker: Option<T::AccountId>,
544 ) -> DispatchResult {
545 let caller = ensure_signed(origin)?;
546 let staker = staker.unwrap_or(caller.clone());
547
548 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
550 let now = frame_system::Pallet::<T>::block_number();
551 ensure!(now > pool_info.expiry_block || caller == staker, BadOrigin);
552
553 let staker_info = PoolStakers::<T>::get(pool_id, &staker).unwrap_or_default();
554 let (mut pool_info, mut staker_info) =
555 Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
556
557 ensure!(staker_info.amount >= amount, Error::<T>::NotEnoughTokens);
559
560 T::AssetsFreezer::decrease_frozen(
562 pool_info.staked_asset_id.clone(),
563 &FreezeReason::Staked.into(),
564 &staker,
565 amount,
566 )?;
567
568 pool_info.total_tokens_staked.ensure_sub_assign(amount)?;
570 Pools::<T>::insert(pool_id, pool_info);
571
572 staker_info.amount.ensure_sub_assign(amount)?;
574
575 if staker_info.amount.is_zero() && staker_info.rewards.is_zero() {
576 PoolStakers::<T>::remove(&pool_id, &staker);
577 } else {
578 PoolStakers::<T>::insert(&pool_id, &staker, staker_info);
579 }
580
581 Self::deposit_event(Event::Unstaked { caller, staker, pool_id, amount });
583
584 Ok(())
585 }
586
587 #[pallet::call_index(3)]
594 pub fn harvest_rewards(
595 origin: OriginFor<T>,
596 pool_id: PoolId,
597 staker: Option<T::AccountId>,
598 ) -> DispatchResult {
599 let caller = ensure_signed(origin)?;
600 let staker = staker.unwrap_or(caller.clone());
601
602 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
604 let now = frame_system::Pallet::<T>::block_number();
605 ensure!(now > pool_info.expiry_block || caller == staker, BadOrigin);
606
607 let staker_info =
608 PoolStakers::<T>::get(pool_id, &staker).ok_or(Error::<T>::NonExistentStaker)?;
609 let (pool_info, mut staker_info) =
610 Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
611
612 T::Assets::transfer(
614 pool_info.reward_asset_id,
615 &pool_info.account,
616 &staker,
617 staker_info.rewards,
618 Preservation::Expendable,
620 )?;
621
622 Self::deposit_event(Event::RewardsHarvested {
624 caller,
625 staker: staker.clone(),
626 pool_id,
627 amount: staker_info.rewards,
628 });
629
630 staker_info.rewards = 0u32.into();
632
633 if staker_info.amount.is_zero() {
634 PoolStakers::<T>::remove(&pool_id, &staker);
635 } else {
636 PoolStakers::<T>::insert(&pool_id, &staker, staker_info);
637 }
638
639 Ok(())
640 }
641
642 #[pallet::call_index(4)]
648 pub fn set_pool_reward_rate_per_block(
649 origin: OriginFor<T>,
650 pool_id: PoolId,
651 new_reward_rate_per_block: T::Balance,
652 ) -> DispatchResult {
653 let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
654 .or_else(|_| ensure_signed(origin))?;
655
656 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
657 ensure!(pool_info.admin == caller, BadOrigin);
658 ensure!(
659 new_reward_rate_per_block > pool_info.reward_rate_per_block,
660 Error::<T>::RewardRateCut
661 );
662
663 let rewards_per_token = Self::reward_per_token(&pool_info)?;
665 let mut pool_info = Self::update_pool_rewards(&pool_info, rewards_per_token)?;
666
667 pool_info.reward_rate_per_block = new_reward_rate_per_block;
668 Pools::<T>::insert(pool_id, pool_info);
669
670 Self::deposit_event(Event::PoolRewardRateModified {
671 pool_id,
672 new_reward_rate_per_block,
673 });
674
675 Ok(())
676 }
677
678 #[pallet::call_index(5)]
682 pub fn set_pool_admin(
683 origin: OriginFor<T>,
684 pool_id: PoolId,
685 new_admin: T::AccountId,
686 ) -> DispatchResult {
687 let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
688 .or_else(|_| ensure_signed(origin))?;
689
690 let mut pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
691 ensure!(pool_info.admin == caller, BadOrigin);
692
693 pool_info.admin = new_admin.clone();
694 Pools::<T>::insert(pool_id, pool_info);
695
696 Self::deposit_event(Event::PoolAdminModified { pool_id, new_admin });
697
698 Ok(())
699 }
700
701 #[pallet::call_index(6)]
707 pub fn set_pool_expiry_block(
708 origin: OriginFor<T>,
709 pool_id: PoolId,
710 new_expiry: DispatchTime<BlockNumberFor<T>>,
711 ) -> DispatchResult {
712 let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
713 .or_else(|_| ensure_signed(origin))?;
714
715 let new_expiry = new_expiry.evaluate(frame_system::Pallet::<T>::block_number());
716 ensure!(
717 new_expiry > frame_system::Pallet::<T>::block_number(),
718 Error::<T>::ExpiryBlockMustBeInTheFuture
719 );
720
721 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
722 ensure!(pool_info.admin == caller, BadOrigin);
723 ensure!(new_expiry > pool_info.expiry_block, Error::<T>::ExpiryCut);
724
725 let reward_per_token = Self::reward_per_token(&pool_info)?;
727 let mut pool_info = Self::update_pool_rewards(&pool_info, reward_per_token)?;
728
729 pool_info.expiry_block = new_expiry;
730 Pools::<T>::insert(pool_id, pool_info);
731
732 Self::deposit_event(Event::PoolExpiryBlockModified {
733 pool_id,
734 new_expiry_block: new_expiry,
735 });
736
737 Ok(())
738 }
739
740 #[pallet::call_index(7)]
746 pub fn deposit_reward_tokens(
747 origin: OriginFor<T>,
748 pool_id: PoolId,
749 amount: T::Balance,
750 ) -> DispatchResult {
751 let caller = ensure_signed(origin)?;
752 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
753 T::Assets::transfer(
754 pool_info.reward_asset_id,
755 &caller,
756 &pool_info.account,
757 amount,
758 Preservation::Preserve,
759 )?;
760 Ok(())
761 }
762
763 #[pallet::call_index(8)]
770 pub fn cleanup_pool(origin: OriginFor<T>, pool_id: PoolId) -> DispatchResult {
771 let who = ensure_signed(origin)?;
772
773 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
774 ensure!(pool_info.admin == who, BadOrigin);
775
776 let stakers = PoolStakers::<T>::iter_key_prefix(pool_id).next();
777 ensure!(stakers.is_none(), Error::<T>::NonEmptyPool);
778
779 let pool_balance = T::Assets::reducible_balance(
780 pool_info.reward_asset_id.clone(),
781 &pool_info.account,
782 Preservation::Expendable,
783 Fortitude::Polite,
784 );
785 T::Assets::transfer(
786 pool_info.reward_asset_id,
787 &pool_info.account,
788 &pool_info.admin,
789 pool_balance,
790 Preservation::Expendable,
791 )?;
792
793 if let Some((who, cost)) = PoolCost::<T>::take(pool_id) {
794 T::Consideration::drop(cost, &who)?;
795 }
796
797 Pools::<T>::remove(pool_id);
798
799 Self::deposit_event(Event::PoolCleanedUp { pool_id });
800
801 Ok(())
802 }
803 }
804
805 impl<T: Config> Pallet<T> {
806 pub fn pool_creation_footprint() -> Footprint {
811 Footprint::from_mel::<(PoolId, PoolInfoFor<T>)>()
812 }
813
814 pub fn pool_account_id(id: &PoolId) -> T::AccountId {
816 T::PalletId::get().into_sub_account_truncating(id)
817 }
818
819 pub fn update_pool_and_staker_rewards(
828 pool_info: &PoolInfoFor<T>,
829 staker_info: &PoolStakerInfo<T::Balance>,
830 ) -> Result<(PoolInfoFor<T>, PoolStakerInfo<T::Balance>), DispatchError> {
831 let reward_per_token = Self::reward_per_token(&pool_info)?;
832 let pool_info = Self::update_pool_rewards(pool_info, reward_per_token)?;
833
834 let mut new_staker_info = staker_info.clone();
835 new_staker_info.rewards = Self::derive_rewards(&staker_info, &reward_per_token)?;
836 new_staker_info.reward_per_token_paid = pool_info.reward_per_token_stored;
837 return Ok((pool_info, new_staker_info));
838 }
839
840 pub fn update_pool_rewards(
849 pool_info: &PoolInfoFor<T>,
850 reward_per_token: T::Balance,
851 ) -> Result<PoolInfoFor<T>, DispatchError> {
852 let mut new_pool_info = pool_info.clone();
853 new_pool_info.last_update_block = frame_system::Pallet::<T>::block_number();
854 new_pool_info.reward_per_token_stored = reward_per_token;
855
856 Ok(new_pool_info)
857 }
858
859 fn reward_per_token(pool_info: &PoolInfoFor<T>) -> Result<T::Balance, DispatchError> {
861 if pool_info.total_tokens_staked.is_zero() {
862 return Ok(pool_info.reward_per_token_stored)
863 }
864
865 let rewardable_blocks_elapsed: u32 =
866 match Self::last_block_reward_applicable(pool_info.expiry_block)
867 .ensure_sub(pool_info.last_update_block)?
868 .try_into()
869 {
870 Ok(b) => b,
871 Err(_) => return Err(Error::<T>::BlockNumberConversionError.into()),
872 };
873
874 Ok(pool_info.reward_per_token_stored.ensure_add(
875 pool_info
876 .reward_rate_per_block
877 .ensure_mul(rewardable_blocks_elapsed.into())?
878 .ensure_mul(PRECISION_SCALING_FACTOR.into())?
879 .ensure_div(pool_info.total_tokens_staked)?,
880 )?)
881 }
882
883 fn derive_rewards(
887 staker_info: &PoolStakerInfo<T::Balance>,
888 reward_per_token: &T::Balance,
889 ) -> Result<T::Balance, DispatchError> {
890 Ok(staker_info
891 .amount
892 .ensure_mul(reward_per_token.ensure_sub(staker_info.reward_per_token_paid)?)?
893 .ensure_div(PRECISION_SCALING_FACTOR.into())?
894 .ensure_add(staker_info.rewards)?)
895 }
896
897 fn last_block_reward_applicable(pool_expiry_block: BlockNumberFor<T>) -> BlockNumberFor<T> {
898 let now = frame_system::Pallet::<T>::block_number();
899 if now < pool_expiry_block {
900 now
901 } else {
902 pool_expiry_block
903 }
904 }
905 }
906}