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 ensure,
88 traits::{
89 fungibles::{Inspect, Mutate},
90 schedule::DispatchTime,
91 tokens::Balance,
92 Consideration, RewardsPool,
93 },
94 PalletId,
95};
96use scale_info::TypeInfo;
97use sp_core::Get;
98use sp_runtime::{
99 traits::{BadOrigin, BlockNumberProvider, EnsureAdd, MaybeDisplay, Zero},
100 DispatchError, DispatchResult,
101};
102use sp_std::boxed::Box;
103
104#[cfg(feature = "runtime-benchmarks")]
105pub mod benchmarking;
106#[cfg(test)]
107mod mock;
108#[cfg(test)]
109mod tests;
110mod weights;
111
112pub use weights::WeightInfo;
113
114pub type PoolId = u32;
116
117pub(crate) const PRECISION_SCALING_FACTOR: u16 = 4096;
119
120pub type PoolInfoFor<T> = PoolInfo<
122 <T as frame_system::Config>::AccountId,
123 <T as Config>::AssetId,
124 <T as Config>::Balance,
125 BlockNumberFor<T>,
126>;
127
128pub type BlockNumberFor<T> =
133 <<T as Config>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
134
135#[derive(Debug, Default, Clone, Decode, Encode, MaxEncodedLen, TypeInfo)]
137pub struct PoolStakerInfo<Balance> {
138 amount: Balance,
140 rewards: Balance,
142 reward_per_token_paid: Balance,
144}
145
146#[derive(Debug, Clone, Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)]
148pub struct PoolInfo<AccountId, AssetId, Balance, BlockNumber> {
149 staked_asset_id: AssetId,
151 reward_asset_id: AssetId,
153 reward_rate_per_block: Balance,
155 expiry_block: BlockNumber,
157 admin: AccountId,
159 total_tokens_staked: Balance,
161 reward_per_token_stored: Balance,
163 last_update_block: BlockNumber,
165 account: AccountId,
167}
168
169sp_api::decl_runtime_apis! {
170 pub trait AssetRewards<Cost: MaybeDisplay + Codec> {
172 fn pool_creation_cost() -> Cost;
176 }
177}
178
179#[frame_support::pallet]
180pub mod pallet {
181 use super::*;
182 use frame_support::{
183 pallet_prelude::*,
184 traits::{
185 fungibles::MutateFreeze,
186 tokens::{AssetId, Fortitude, Preservation},
187 Consideration, Footprint, RewardsPool,
188 },
189 };
190 use frame_system::pallet_prelude::{
191 ensure_signed, BlockNumberFor as SystemBlockNumberFor, OriginFor,
192 };
193 use sp_runtime::{
194 traits::{
195 AccountIdConversion, BadOrigin, EnsureAdd, EnsureAddAssign, EnsureDiv, EnsureMul,
196 EnsureSub, EnsureSubAssign,
197 },
198 DispatchResult,
199 };
200
201 #[pallet::pallet]
202 pub struct Pallet<T>(_);
203
204 #[pallet::composite_enum]
206 pub enum FreezeReason {
207 #[codec(index = 0)]
209 Staked,
210 }
211
212 #[pallet::composite_enum]
214 pub enum HoldReason {
215 #[codec(index = 0)]
217 PoolCreation,
218 }
219
220 #[pallet::config]
221 pub trait Config: frame_system::Config {
222 #[allow(deprecated)]
224 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
225
226 #[pallet::constant]
230 type PalletId: Get<PalletId>;
231
232 type AssetId: AssetId + Member + Parameter;
234
235 type Balance: Balance + TypeInfo;
237
238 type CreatePoolOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::AccountId>;
242
243 type Assets: Inspect<Self::AccountId, AssetId = Self::AssetId, Balance = Self::Balance>
246 + Mutate<Self::AccountId>;
247
248 type AssetsFreezer: MutateFreeze<
250 Self::AccountId,
251 Id = Self::RuntimeFreezeReason,
252 AssetId = Self::AssetId,
253 Balance = Self::Balance,
254 >;
255
256 type RuntimeFreezeReason: From<FreezeReason>;
258
259 type Consideration: Consideration<Self::AccountId, Footprint>;
265
266 type WeightInfo: WeightInfo;
268
269 type BlockNumberProvider: BlockNumberProvider;
281
282 #[cfg(feature = "runtime-benchmarks")]
284 type BenchmarkHelper: benchmarking::BenchmarkHelper<Self::AssetId>;
285 }
286
287 #[pallet::storage]
289 pub type PoolStakers<T: Config> = StorageDoubleMap<
290 _,
291 Blake2_128Concat,
292 PoolId,
293 Blake2_128Concat,
294 T::AccountId,
295 PoolStakerInfo<T::Balance>,
296 >;
297
298 #[pallet::storage]
300 pub type Pools<T: Config> = StorageMap<_, Blake2_128Concat, PoolId, PoolInfoFor<T>>;
301
302 #[pallet::storage]
307 pub type PoolCost<T: Config> =
308 StorageMap<_, Blake2_128Concat, PoolId, (T::AccountId, T::Consideration)>;
309
310 #[pallet::storage]
314 pub type NextPoolId<T: Config> = StorageValue<_, PoolId, ValueQuery>;
315
316 #[pallet::event]
317 #[pallet::generate_deposit(pub(super) fn deposit_event)]
318 pub enum Event<T: Config> {
319 Staked {
321 staker: T::AccountId,
323 pool_id: PoolId,
325 amount: T::Balance,
327 },
328 Unstaked {
330 caller: T::AccountId,
332 staker: T::AccountId,
334 pool_id: PoolId,
336 amount: T::Balance,
338 },
339 RewardsHarvested {
341 caller: T::AccountId,
343 staker: T::AccountId,
345 pool_id: PoolId,
347 amount: T::Balance,
349 },
350 PoolCreated {
352 creator: T::AccountId,
354 pool_id: PoolId,
356 staked_asset_id: T::AssetId,
358 reward_asset_id: T::AssetId,
360 reward_rate_per_block: T::Balance,
362 expiry_block: BlockNumberFor<T>,
364 admin: T::AccountId,
366 },
367 PoolRewardRateModified {
369 pool_id: PoolId,
371 new_reward_rate_per_block: T::Balance,
373 },
374 PoolAdminModified {
376 pool_id: PoolId,
378 new_admin: T::AccountId,
380 },
381 PoolExpiryBlockModified {
383 pool_id: PoolId,
385 new_expiry_block: BlockNumberFor<T>,
387 },
388 PoolCleanedUp {
390 pool_id: PoolId,
392 },
393 }
394
395 #[pallet::error]
396 pub enum Error<T> {
397 NotEnoughTokens,
399 NonExistentPool,
401 NonExistentStaker,
403 NonExistentAsset,
405 BlockNumberConversionError,
407 ExpiryBlockMustBeInTheFuture,
409 InsufficientFunds,
411 ExpiryCut,
413 RewardRateCut,
415 NonEmptyPool,
417 }
418
419 #[pallet::hooks]
420 impl<T: Config> Hooks<SystemBlockNumberFor<T>> for Pallet<T> {
421 fn integrity_test() {
422 let pool_id: PoolId = 1;
424 assert!(
425 <frame_support::PalletId as AccountIdConversion<T::AccountId>>::try_into_sub_account(
426 &T::PalletId::get(), pool_id,
427 )
428 .is_some()
429 );
430 }
431 }
432
433 #[pallet::call(weight(<T as Config>::WeightInfo))]
435 impl<T: Config> Pallet<T> {
436 #[pallet::call_index(0)]
449 pub fn create_pool(
450 origin: OriginFor<T>,
451 staked_asset_id: Box<T::AssetId>,
452 reward_asset_id: Box<T::AssetId>,
453 reward_rate_per_block: T::Balance,
454 expiry: DispatchTime<BlockNumberFor<T>>,
455 admin: Option<T::AccountId>,
456 ) -> DispatchResult {
457 let creator = T::CreatePoolOrigin::ensure_origin(origin)?;
458 <Self as RewardsPool<_>>::create_pool(
459 &creator,
460 *staked_asset_id,
461 *reward_asset_id,
462 reward_rate_per_block,
463 expiry,
464 &admin.unwrap_or_else(|| creator.clone()),
465 )?;
466 Ok(())
467 }
468
469 #[pallet::call_index(1)]
473 pub fn stake(origin: OriginFor<T>, pool_id: PoolId, amount: T::Balance) -> DispatchResult {
474 let staker = ensure_signed(origin)?;
475
476 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
478 let staker_info = PoolStakers::<T>::get(pool_id, &staker).unwrap_or_default();
479 let (mut pool_info, mut staker_info) =
480 Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
481
482 T::AssetsFreezer::increase_frozen(
483 pool_info.staked_asset_id.clone(),
484 &FreezeReason::Staked.into(),
485 &staker,
486 amount,
487 )?;
488
489 pool_info.total_tokens_staked.ensure_add_assign(amount)?;
491
492 Pools::<T>::insert(pool_id, pool_info);
493
494 staker_info.amount.ensure_add_assign(amount)?;
496 PoolStakers::<T>::insert(pool_id, &staker, staker_info);
497
498 Self::deposit_event(Event::Staked { staker, pool_id, amount });
500
501 Ok(())
502 }
503
504 #[pallet::call_index(2)]
514 pub fn unstake(
515 origin: OriginFor<T>,
516 pool_id: PoolId,
517 amount: T::Balance,
518 staker: Option<T::AccountId>,
519 ) -> DispatchResult {
520 let caller = ensure_signed(origin)?;
521 let staker = staker.unwrap_or(caller.clone());
522
523 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
525 let now = T::BlockNumberProvider::current_block_number();
526 ensure!(now > pool_info.expiry_block || caller == staker, BadOrigin);
527
528 let staker_info = PoolStakers::<T>::get(pool_id, &staker).unwrap_or_default();
529 let (mut pool_info, mut staker_info) =
530 Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
531
532 ensure!(staker_info.amount >= amount, Error::<T>::NotEnoughTokens);
534
535 T::AssetsFreezer::decrease_frozen(
537 pool_info.staked_asset_id.clone(),
538 &FreezeReason::Staked.into(),
539 &staker,
540 amount,
541 )?;
542
543 pool_info.total_tokens_staked.ensure_sub_assign(amount)?;
545 Pools::<T>::insert(pool_id, pool_info);
546
547 staker_info.amount.ensure_sub_assign(amount)?;
549
550 if staker_info.amount.is_zero() && staker_info.rewards.is_zero() {
551 PoolStakers::<T>::remove(&pool_id, &staker);
552 } else {
553 PoolStakers::<T>::insert(&pool_id, &staker, staker_info);
554 }
555
556 Self::deposit_event(Event::Unstaked { caller, staker, pool_id, amount });
558
559 Ok(())
560 }
561
562 #[pallet::call_index(3)]
569 pub fn harvest_rewards(
570 origin: OriginFor<T>,
571 pool_id: PoolId,
572 staker: Option<T::AccountId>,
573 ) -> DispatchResult {
574 let caller = ensure_signed(origin)?;
575 let staker = staker.unwrap_or(caller.clone());
576
577 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
579 let now = T::BlockNumberProvider::current_block_number();
580 ensure!(now > pool_info.expiry_block || caller == staker, BadOrigin);
581
582 let staker_info =
583 PoolStakers::<T>::get(pool_id, &staker).ok_or(Error::<T>::NonExistentStaker)?;
584 let (pool_info, mut staker_info) =
585 Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
586
587 T::Assets::transfer(
589 pool_info.reward_asset_id,
590 &pool_info.account,
591 &staker,
592 staker_info.rewards,
593 Preservation::Expendable,
595 )?;
596
597 Self::deposit_event(Event::RewardsHarvested {
599 caller,
600 staker: staker.clone(),
601 pool_id,
602 amount: staker_info.rewards,
603 });
604
605 staker_info.rewards = 0u32.into();
607
608 if staker_info.amount.is_zero() {
609 PoolStakers::<T>::remove(&pool_id, &staker);
610 } else {
611 PoolStakers::<T>::insert(&pool_id, &staker, staker_info);
612 }
613
614 Ok(())
615 }
616
617 #[pallet::call_index(4)]
623 pub fn set_pool_reward_rate_per_block(
624 origin: OriginFor<T>,
625 pool_id: PoolId,
626 new_reward_rate_per_block: T::Balance,
627 ) -> DispatchResult {
628 let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
629 .or_else(|_| ensure_signed(origin))?;
630 <Self as RewardsPool<_>>::set_pool_reward_rate_per_block(
631 &caller,
632 pool_id,
633 new_reward_rate_per_block,
634 )
635 }
636
637 #[pallet::call_index(5)]
641 pub fn set_pool_admin(
642 origin: OriginFor<T>,
643 pool_id: PoolId,
644 new_admin: T::AccountId,
645 ) -> DispatchResult {
646 let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
647 .or_else(|_| ensure_signed(origin))?;
648 <Self as RewardsPool<_>>::set_pool_admin(&caller, pool_id, new_admin)
649 }
650
651 #[pallet::call_index(6)]
657 pub fn set_pool_expiry_block(
658 origin: OriginFor<T>,
659 pool_id: PoolId,
660 new_expiry: DispatchTime<BlockNumberFor<T>>,
661 ) -> DispatchResult {
662 let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
663 .or_else(|_| ensure_signed(origin))?;
664 <Self as RewardsPool<_>>::set_pool_expiry_block(&caller, pool_id, new_expiry)
665 }
666
667 #[pallet::call_index(7)]
673 pub fn deposit_reward_tokens(
674 origin: OriginFor<T>,
675 pool_id: PoolId,
676 amount: T::Balance,
677 ) -> DispatchResult {
678 let caller = ensure_signed(origin)?;
679 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
680 T::Assets::transfer(
681 pool_info.reward_asset_id,
682 &caller,
683 &pool_info.account,
684 amount,
685 Preservation::Preserve,
686 )?;
687 Ok(())
688 }
689
690 #[pallet::call_index(8)]
697 pub fn cleanup_pool(origin: OriginFor<T>, pool_id: PoolId) -> DispatchResult {
698 let who = ensure_signed(origin)?;
699
700 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
701 ensure!(pool_info.admin == who, BadOrigin);
702
703 let stakers = PoolStakers::<T>::iter_key_prefix(pool_id).next();
704 ensure!(stakers.is_none(), Error::<T>::NonEmptyPool);
705
706 let pool_balance = T::Assets::reducible_balance(
707 pool_info.reward_asset_id.clone(),
708 &pool_info.account,
709 Preservation::Expendable,
710 Fortitude::Polite,
711 );
712 T::Assets::transfer(
713 pool_info.reward_asset_id,
714 &pool_info.account,
715 &pool_info.admin,
716 pool_balance,
717 Preservation::Expendable,
718 )?;
719
720 if let Some((who, cost)) = PoolCost::<T>::take(pool_id) {
721 T::Consideration::drop(cost, &who)?;
722 }
723
724 Pools::<T>::remove(pool_id);
725
726 Self::deposit_event(Event::PoolCleanedUp { pool_id });
727
728 Ok(())
729 }
730 }
731
732 impl<T: Config> Pallet<T> {
733 pub fn pool_creation_footprint() -> Footprint {
738 Footprint::from_mel::<(PoolId, PoolInfoFor<T>)>()
739 }
740
741 pub fn pool_account_id(id: &PoolId) -> T::AccountId {
743 T::PalletId::get().into_sub_account_truncating(id)
744 }
745
746 pub fn update_pool_and_staker_rewards(
755 pool_info: &PoolInfoFor<T>,
756 staker_info: &PoolStakerInfo<T::Balance>,
757 ) -> Result<(PoolInfoFor<T>, PoolStakerInfo<T::Balance>), DispatchError> {
758 let reward_per_token = Self::reward_per_token(&pool_info)?;
759 let pool_info = Self::update_pool_rewards(pool_info, reward_per_token)?;
760
761 let mut new_staker_info = staker_info.clone();
762 new_staker_info.rewards = Self::derive_rewards(&staker_info, &reward_per_token)?;
763 new_staker_info.reward_per_token_paid = pool_info.reward_per_token_stored;
764 return Ok((pool_info, new_staker_info));
765 }
766
767 pub fn update_pool_rewards(
776 pool_info: &PoolInfoFor<T>,
777 reward_per_token: T::Balance,
778 ) -> Result<PoolInfoFor<T>, DispatchError> {
779 let mut new_pool_info = pool_info.clone();
780 new_pool_info.last_update_block = T::BlockNumberProvider::current_block_number();
781 new_pool_info.reward_per_token_stored = reward_per_token;
782
783 Ok(new_pool_info)
784 }
785
786 pub(super) fn reward_per_token(
788 pool_info: &PoolInfoFor<T>,
789 ) -> Result<T::Balance, DispatchError> {
790 if pool_info.total_tokens_staked.is_zero() {
791 return Ok(pool_info.reward_per_token_stored)
792 }
793
794 let rewardable_blocks_elapsed: u32 =
795 match Self::last_block_reward_applicable(pool_info.expiry_block)
796 .ensure_sub(pool_info.last_update_block)?
797 .try_into()
798 {
799 Ok(b) => b,
800 Err(_) => return Err(Error::<T>::BlockNumberConversionError.into()),
801 };
802
803 Ok(pool_info.reward_per_token_stored.ensure_add(
804 pool_info
805 .reward_rate_per_block
806 .ensure_mul(rewardable_blocks_elapsed.into())?
807 .ensure_mul(PRECISION_SCALING_FACTOR.into())?
808 .ensure_div(pool_info.total_tokens_staked)?,
809 )?)
810 }
811
812 fn derive_rewards(
816 staker_info: &PoolStakerInfo<T::Balance>,
817 reward_per_token: &T::Balance,
818 ) -> Result<T::Balance, DispatchError> {
819 Ok(staker_info
820 .amount
821 .ensure_mul(reward_per_token.ensure_sub(staker_info.reward_per_token_paid)?)?
822 .ensure_div(PRECISION_SCALING_FACTOR.into())?
823 .ensure_add(staker_info.rewards)?)
824 }
825
826 fn last_block_reward_applicable(pool_expiry_block: BlockNumberFor<T>) -> BlockNumberFor<T> {
827 let now = T::BlockNumberProvider::current_block_number();
828 if now < pool_expiry_block {
829 now
830 } else {
831 pool_expiry_block
832 }
833 }
834 }
835}
836
837impl<T: Config> RewardsPool<T::AccountId> for Pallet<T> {
838 type AssetId = T::AssetId;
839 type BlockNumber = BlockNumberFor<T>;
840 type PoolId = PoolId;
841 type Balance = T::Balance;
842
843 fn create_pool(
844 creator: &T::AccountId,
845 staked_asset_id: T::AssetId,
846 reward_asset_id: T::AssetId,
847 reward_rate_per_block: T::Balance,
848 expiry: DispatchTime<BlockNumberFor<T>>,
849 admin: &T::AccountId,
850 ) -> Result<PoolId, DispatchError> {
851 ensure!(T::Assets::asset_exists(staked_asset_id.clone()), Error::<T>::NonExistentAsset);
853 ensure!(T::Assets::asset_exists(reward_asset_id.clone()), Error::<T>::NonExistentAsset);
854
855 let now = T::BlockNumberProvider::current_block_number();
857 let expiry_block = expiry.evaluate(now);
858 ensure!(expiry_block > now, Error::<T>::ExpiryBlockMustBeInTheFuture);
859
860 let pool_id = NextPoolId::<T>::try_mutate(|id| -> Result<PoolId, DispatchError> {
861 let current_id = *id;
862 *id = id.ensure_add(1)?;
863 Ok(current_id)
864 })?;
865
866 let footprint = Self::pool_creation_footprint();
867 let cost = T::Consideration::new(creator, footprint)?;
868 PoolCost::<T>::insert(pool_id, (creator.clone(), cost));
869
870 let pool = PoolInfoFor::<T> {
872 staked_asset_id: staked_asset_id.clone(),
873 reward_asset_id: reward_asset_id.clone(),
874 reward_rate_per_block,
875 total_tokens_staked: 0u32.into(),
876 reward_per_token_stored: 0u32.into(),
877 last_update_block: 0u32.into(),
878 expiry_block,
879 admin: admin.clone(),
880 account: Self::pool_account_id(&pool_id),
881 };
882
883 Pools::<T>::insert(pool_id, pool);
885
886 Self::deposit_event(Event::PoolCreated {
888 creator: creator.clone(),
889 pool_id,
890 staked_asset_id,
891 reward_asset_id,
892 reward_rate_per_block,
893 expiry_block,
894 admin: admin.clone(),
895 });
896
897 Ok(pool_id)
898 }
899
900 fn set_pool_reward_rate_per_block(
901 admin: &T::AccountId,
902 pool_id: PoolId,
903 new_reward_rate_per_block: T::Balance,
904 ) -> DispatchResult {
905 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
906 ensure!(pool_info.admin == *admin, BadOrigin);
907 ensure!(
908 new_reward_rate_per_block > pool_info.reward_rate_per_block,
909 Error::<T>::RewardRateCut
910 );
911
912 let rewards_per_token = Self::reward_per_token(&pool_info)?;
914 let mut pool_info = Self::update_pool_rewards(&pool_info, rewards_per_token)?;
915
916 pool_info.reward_rate_per_block = new_reward_rate_per_block;
917 Pools::<T>::insert(pool_id, pool_info);
918
919 Self::deposit_event(Event::PoolRewardRateModified { pool_id, new_reward_rate_per_block });
920
921 Ok(())
922 }
923
924 fn set_pool_admin(
925 admin: &T::AccountId,
926 pool_id: PoolId,
927 new_admin: T::AccountId,
928 ) -> DispatchResult {
929 let mut pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
930 ensure!(pool_info.admin == *admin, BadOrigin);
931
932 pool_info.admin = new_admin.clone();
933 Pools::<T>::insert(pool_id, pool_info);
934
935 Self::deposit_event(Event::PoolAdminModified { pool_id, new_admin });
936
937 Ok(())
938 }
939
940 fn set_pool_expiry_block(
941 admin: &T::AccountId,
942 pool_id: PoolId,
943 new_expiry: DispatchTime<BlockNumberFor<T>>,
944 ) -> DispatchResult {
945 let now = T::BlockNumberProvider::current_block_number();
946 let new_expiry_block = new_expiry.evaluate(now);
947 ensure!(new_expiry_block > now, Error::<T>::ExpiryBlockMustBeInTheFuture);
948
949 let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
950 ensure!(pool_info.admin == *admin, BadOrigin);
951 ensure!(new_expiry_block > pool_info.expiry_block, Error::<T>::ExpiryCut);
952
953 let reward_per_token = Self::reward_per_token(&pool_info)?;
955 let mut pool_info = Self::update_pool_rewards(&pool_info, reward_per_token)?;
956
957 pool_info.expiry_block = new_expiry_block;
958 Pools::<T>::insert(pool_id, pool_info);
959
960 Self::deposit_event(Event::PoolExpiryBlockModified { pool_id, new_expiry_block });
961
962 Ok(())
963 }
964}