1#![cfg_attr(not(feature = "std"), no_std)]
83
84extern crate alloc;
85
86pub mod migrations;
87pub mod weights;
88
89#[cfg(feature = "runtime-benchmarks")]
90mod benchmarking;
91#[cfg(test)]
92mod mock;
93#[cfg(test)]
94mod tests;
95
96pub use pallet::*;
97pub use weights::WeightInfo;
98
99#[cfg(feature = "runtime-benchmarks")]
105pub trait BenchmarkHelper<AssetId, AccountId> {
106 fn get_asset_id(asset_index: u32) -> AssetId;
108 fn create_asset(asset_id: AssetId, owner: &AccountId, decimals: u8);
110}
111
112#[frame_support::pallet]
113pub mod pallet {
114 pub use frame_support::traits::tokens::stable::PsmInterface;
115
116 use alloc::collections::btree_map::BTreeMap;
117 use codec::DecodeWithMemTracking;
118 use frame_support::{
119 pallet_prelude::*,
120 traits::{
121 fungible::{
122 metadata::Inspect as FungibleMetadataInspect, Inspect as FungibleInspect,
123 Mutate as FungibleMutate,
124 },
125 fungibles::{
126 metadata::Inspect as FungiblesMetadataInspect, Inspect as FungiblesInspect,
127 Mutate as FungiblesMutate,
128 },
129 tokens::{Fortitude, Precision, Preservation},
130 },
131 DefaultNoBound, PalletId,
132 };
133 use frame_system::pallet_prelude::*;
134 use sp_runtime::{
135 traits::{AccountIdConversion, CheckedDiv, CheckedMul, Saturating, Zero},
136 Perbill, Permill,
137 };
138
139 use crate::WeightInfo;
140
141 #[derive(
143 Encode,
144 Decode,
145 DecodeWithMemTracking,
146 MaxEncodedLen,
147 TypeInfo,
148 Clone,
149 Copy,
150 PartialEq,
151 Eq,
152 Debug,
153 Default,
154 )]
155 pub enum CircuitBreakerLevel {
156 #[default]
158 AllEnabled,
159 MintingDisabled,
161 AllDisabled,
163 }
164
165 impl CircuitBreakerLevel {
166 pub const fn allows_minting(&self) -> bool {
168 matches!(self, CircuitBreakerLevel::AllEnabled)
169 }
170
171 pub const fn allows_redemption(&self) -> bool {
173 !matches!(self, CircuitBreakerLevel::AllDisabled)
174 }
175 }
176
177 #[derive(
182 Encode,
183 Decode,
184 DecodeWithMemTracking,
185 MaxEncodedLen,
186 TypeInfo,
187 Clone,
188 Copy,
189 PartialEq,
190 Eq,
191 Debug,
192 Default,
193 )]
194 pub enum PsmManagerLevel {
195 #[default]
198 Full,
199 Emergency,
202 }
203
204 impl PsmManagerLevel {
205 pub const fn can_set_fees(&self) -> bool {
207 matches!(self, PsmManagerLevel::Full)
208 }
209
210 pub const fn can_set_circuit_breaker(&self) -> bool {
213 matches!(self, PsmManagerLevel::Full | PsmManagerLevel::Emergency)
214 }
215
216 pub const fn can_set_max_psm_debt(&self) -> bool {
219 matches!(self, PsmManagerLevel::Full | PsmManagerLevel::Emergency)
220 }
221
222 pub const fn can_set_asset_ceiling(&self) -> bool {
225 matches!(self, PsmManagerLevel::Full | PsmManagerLevel::Emergency)
226 }
227
228 pub const fn can_manage_assets(&self) -> bool {
230 matches!(self, PsmManagerLevel::Full)
231 }
232 }
233
234 pub(crate) type BalanceOf<T> = <<T as Config>::Fungibles as FungiblesInspect<
235 <T as frame_system::Config>::AccountId,
236 >>::Balance;
237
238 pub(crate) struct DefaultFee;
240 impl Get<Permill> for DefaultFee {
241 fn get() -> Permill {
242 Permill::from_parts(5_000)
243 }
244 }
245
246 pub const MAX_DECIMALS_DIFF: u32 = 24;
250
251 #[pallet::config]
252 pub trait Config: frame_system::Config {
253 type Fungibles: FungiblesMutate<Self::AccountId, AssetId = Self::AssetId>
255 + FungiblesMetadataInspect<Self::AccountId>;
256
257 type AssetId: Parameter + Member + Clone + MaybeSerializeDeserialize + MaxEncodedLen + Ord;
259
260 type MaximumIssuance: Get<BalanceOf<Self>>;
262
263 type ManagerOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = PsmManagerLevel>;
270
271 type WeightInfo: WeightInfo;
273
274 type InternalAsset: FungibleMutate<Self::AccountId, Balance = BalanceOf<Self>>
279 + FungibleMetadataInspect<Self::AccountId>;
280
281 type FeeDestination: Get<Self::AccountId>;
286
287 #[pallet::constant]
289 type PalletId: Get<PalletId>;
290
291 #[pallet::constant]
293 type MinSwapAmount: Get<BalanceOf<Self>>;
294
295 #[pallet::constant]
297 type MaxExternalAssets: Get<u32>;
298
299 #[cfg(feature = "runtime-benchmarks")]
301 type BenchmarkHelper: crate::BenchmarkHelper<Self::AssetId, Self::AccountId>;
302 }
303
304 const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
306
307 #[pallet::pallet]
308 #[pallet::storage_version(STORAGE_VERSION)]
309 pub struct Pallet<T>(_);
310
311 #[pallet::hooks]
312 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
313 fn integrity_test() {
314 assert!(!T::MinSwapAmount::get().is_zero(), "MinSwapAmount must be greater than zero");
315 }
316
317 #[cfg(feature = "try-runtime")]
318 fn try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
319 Self::do_try_state()
320 }
321 }
322
323 #[pallet::storage]
325 pub type PsmDebt<T: Config> =
326 StorageMap<_, Blake2_128Concat, T::AssetId, BalanceOf<T>, ValueQuery>;
327
328 #[pallet::storage]
330 pub(crate) type MintingFee<T: Config> =
331 StorageMap<_, Blake2_128Concat, T::AssetId, Permill, ValueQuery, DefaultFee>;
332
333 #[pallet::storage]
335 pub(crate) type RedemptionFee<T: Config> =
336 StorageMap<_, Blake2_128Concat, T::AssetId, Permill, ValueQuery, DefaultFee>;
337
338 #[pallet::storage]
340 pub(crate) type MaxPsmDebtOfTotal<T: Config> = StorageValue<_, Permill, ValueQuery>;
341
342 #[pallet::storage]
345 pub(crate) type AssetCeilingWeight<T: Config> =
346 StorageMap<_, Blake2_128Concat, T::AssetId, Permill, ValueQuery>;
347
348 #[pallet::storage]
351 pub(crate) type ExternalAssets<T: Config> =
352 CountedStorageMap<_, Blake2_128Concat, T::AssetId, CircuitBreakerLevel, OptionQuery>;
353
354 #[pallet::storage]
357 pub(crate) type ExternalDecimals<T: Config> =
358 StorageMap<_, Blake2_128Concat, T::AssetId, u8, OptionQuery>;
359
360 #[pallet::storage]
363 pub(crate) type InternalDecimals<T: Config> = StorageValue<_, u8, OptionQuery>;
364
365 #[pallet::genesis_config]
367 #[derive(DefaultNoBound)]
368 pub struct GenesisConfig<T: Config> {
369 pub max_psm_debt_of_total: Permill,
371 pub asset_configs: BTreeMap<T::AssetId, (Permill, Permill, Permill)>,
374 #[serde(skip)]
375 pub _marker: core::marker::PhantomData<T>,
376 }
377
378 #[pallet::genesis_build]
379 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
380 fn build(&self) {
381 assert!(
382 self.asset_configs.len() as u32 <= T::MaxExternalAssets::get(),
383 "PSM genesis: asset_configs ({}) exceeds MaxExternalAssets ({})",
384 self.asset_configs.len(),
385 T::MaxExternalAssets::get(),
386 );
387 MaxPsmDebtOfTotal::<T>::put(self.max_psm_debt_of_total);
388 let internal_decimals = T::InternalAsset::decimals();
389 InternalDecimals::<T>::put(internal_decimals);
390 for (asset_id, (minting_fee, redemption_fee, ceiling_weight)) in &self.asset_configs {
391 let asset_decimals = T::Fungibles::decimals(asset_id.clone());
392 let diff = asset_decimals.abs_diff(internal_decimals) as u32;
393 assert!(
394 diff <= MAX_DECIMALS_DIFF,
395 "PSM genesis: asset {:?} decimals diff ({}) exceeds MAX_DECIMALS_DIFF ({})",
396 asset_id,
397 diff,
398 MAX_DECIMALS_DIFF,
399 );
400 ExternalAssets::<T>::insert(asset_id, CircuitBreakerLevel::AllEnabled);
401 ExternalDecimals::<T>::insert(asset_id, asset_decimals);
402 MintingFee::<T>::insert(asset_id, minting_fee);
403 RedemptionFee::<T>::insert(asset_id, redemption_fee);
404 AssetCeilingWeight::<T>::insert(asset_id, ceiling_weight);
405 }
406 Pallet::<T>::ensure_account_exists(&Pallet::<T>::account_id());
407 Pallet::<T>::ensure_account_exists(&T::FeeDestination::get());
408 }
409 }
410
411 #[pallet::event]
412 #[pallet::generate_deposit(pub(super) fn deposit_event)]
413 pub enum Event<T: Config> {
414 Minted {
416 who: T::AccountId,
417 asset_id: T::AssetId,
418 external_amount: BalanceOf<T>,
419 received: BalanceOf<T>,
420 fee: BalanceOf<T>,
421 },
422 Redeemed {
424 who: T::AccountId,
425 asset_id: T::AssetId,
426 paid: BalanceOf<T>,
427 external_received: BalanceOf<T>,
428 fee: BalanceOf<T>,
429 },
430 MintingFeeUpdated { asset_id: T::AssetId, old_value: Permill, new_value: Permill },
432 RedemptionFeeUpdated { asset_id: T::AssetId, old_value: Permill, new_value: Permill },
434 MaxPsmDebtOfTotalUpdated { old_value: Permill, new_value: Permill },
436 AssetCeilingWeightUpdated { asset_id: T::AssetId, old_value: Permill, new_value: Permill },
438 AssetStatusUpdated { asset_id: T::AssetId, status: CircuitBreakerLevel },
440 ExternalAssetAdded { asset_id: T::AssetId },
442 ExternalAssetRemoved { asset_id: T::AssetId },
444 }
445
446 #[pallet::error]
447 pub enum Error<T> {
448 InsufficientReserve,
450 ExceedsMaxPsmDebt,
452 BelowMinimumSwap,
454 MintingStopped,
456 AllSwapsStopped,
458 UnsupportedAsset,
460 ExceedsMaxIssuance,
462 AssetAlreadyApproved,
464 AssetDoesNotExist,
466 AssetNotApproved,
468 AssetHasDebt,
470 InsufficientPrivilege,
472 TooManyAssets,
474 DecimalsMismatch,
476 DecimalsRangeExceeded,
478 ConversionOverflow,
480 AmountTooSmallAfterConversion,
482 Unexpected,
484 }
485
486 #[pallet::call]
487 impl<T: Config> Pallet<T> {
488 #[pallet::call_index(0)]
524 #[pallet::weight(T::WeightInfo::mint(T::MaxExternalAssets::get()))]
525 pub fn mint(
526 origin: OriginFor<T>,
527 asset_id: T::AssetId,
528 external_amount: BalanceOf<T>,
529 ) -> DispatchResult {
530 let who = ensure_signed(origin)?;
531
532 let asset_status =
534 ExternalAssets::<T>::get(&asset_id).ok_or(Error::<T>::UnsupportedAsset)?;
535 ensure!(asset_status.allows_minting(), Error::<T>::MintingStopped);
536
537 let (ext_decimals, internal_decimals) = Self::ensure_decimals_match(asset_id.clone())?;
539
540 let internal_equivalent =
542 Self::external_to_internal(external_amount, ext_decimals, internal_decimals)?;
543 ensure!(!internal_equivalent.is_zero(), Error::<T>::AmountTooSmallAfterConversion);
544 ensure!(internal_equivalent >= T::MinSwapAmount::get(), Error::<T>::BelowMinimumSwap);
545
546 let effective_external =
549 Self::internal_to_external(internal_equivalent, ext_decimals, internal_decimals)?;
550
551 let fee = MintingFee::<T>::get(&asset_id).mul_ceil(internal_equivalent);
552 let internal_to_user = internal_equivalent.saturating_sub(fee);
553
554 let current_total_issuance = T::InternalAsset::total_issuance();
556 let max_issuance = T::MaximumIssuance::get();
557 ensure!(
558 current_total_issuance.saturating_add(internal_equivalent) <= max_issuance,
559 Error::<T>::ExceedsMaxIssuance
560 );
561
562 let current_total_psm_debt = Self::total_psm_debt();
564 let max_psm = Self::max_psm_debt();
565 ensure!(
566 current_total_psm_debt.saturating_add(internal_equivalent) <= max_psm,
567 Error::<T>::ExceedsMaxPsmDebt
568 );
569
570 let current_debt = PsmDebt::<T>::get(&asset_id);
572 let max_debt = Self::max_asset_debt(asset_id.clone());
573 let new_debt = current_debt.saturating_add(internal_equivalent);
574 ensure!(new_debt <= max_debt, Error::<T>::ExceedsMaxPsmDebt);
575
576 let psm_account = Self::account_id();
577
578 T::Fungibles::transfer(
579 asset_id.clone(),
580 &who,
581 &psm_account,
582 effective_external,
583 Preservation::Expendable,
584 )?;
585 T::InternalAsset::mint_into(&who, internal_to_user)?;
586 if !fee.is_zero() {
587 T::InternalAsset::mint_into(&T::FeeDestination::get(), fee)?;
588 }
589
590 PsmDebt::<T>::insert(&asset_id, new_debt);
591
592 Self::deposit_event(Event::Minted {
593 who,
594 asset_id,
595 external_amount: effective_external,
596 received: internal_to_user,
597 fee,
598 });
599
600 Ok(())
601 }
602
603 #[pallet::call_index(1)]
636 #[pallet::weight(T::WeightInfo::redeem())]
637 pub fn redeem(
638 origin: OriginFor<T>,
639 asset_id: T::AssetId,
640 amount: BalanceOf<T>,
641 ) -> DispatchResult {
642 let who = ensure_signed(origin)?;
643
644 let asset_status =
646 ExternalAssets::<T>::get(&asset_id).ok_or(Error::<T>::UnsupportedAsset)?;
647 ensure!(asset_status.allows_redemption(), Error::<T>::AllSwapsStopped);
648
649 let (ext_decimals, internal_decimals) = Self::ensure_decimals_match(asset_id.clone())?;
651
652 ensure!(amount >= T::MinSwapAmount::get(), Error::<T>::BelowMinimumSwap);
653
654 let fee = RedemptionFee::<T>::get(&asset_id).mul_ceil(amount);
655 let internal_net = amount.saturating_sub(fee);
656
657 let external_out =
663 Self::internal_to_external(internal_net, ext_decimals, internal_decimals)?;
664 ensure!(
667 internal_net.is_zero() || !external_out.is_zero(),
668 Error::<T>::AmountTooSmallAfterConversion
669 );
670 let effective_internal_net =
671 Self::external_to_internal(external_out, ext_decimals, internal_decimals)?;
672
673 let current_debt = PsmDebt::<T>::get(&asset_id);
676 ensure!(current_debt >= effective_internal_net, Error::<T>::InsufficientReserve);
677
678 let reserve = Self::get_reserve(asset_id.clone());
679 if reserve < external_out {
680 defensive!("PSM reserve is less than expected output amount");
681 return Err(Error::<T>::Unexpected.into());
682 }
683
684 if !fee.is_zero() {
687 T::InternalAsset::transfer(
688 &who,
689 &T::FeeDestination::get(),
690 fee,
691 Preservation::Expendable,
692 )?;
693 }
694
695 if !effective_internal_net.is_zero() {
696 T::InternalAsset::burn_from(
697 &who,
698 effective_internal_net,
699 Preservation::Expendable,
700 Precision::Exact,
701 Fortitude::Polite,
702 )?;
703 }
704
705 let psm_account = Self::account_id();
706 if !external_out.is_zero() {
707 T::Fungibles::transfer(
708 asset_id.clone(),
709 &psm_account,
710 &who,
711 external_out,
712 Preservation::Expendable,
713 )?;
714 }
715
716 PsmDebt::<T>::mutate(&asset_id, |debt| {
717 *debt = debt.saturating_sub(effective_internal_net);
718 });
719
720 Self::deposit_event(Event::Redeemed {
721 who,
722 asset_id,
723 paid: effective_internal_net.saturating_add(fee),
724 external_received: external_out,
725 fee,
726 });
727
728 Ok(())
729 }
730
731 #[pallet::call_index(2)]
746 #[pallet::weight(T::WeightInfo::set_minting_fee())]
747 pub fn set_minting_fee(
748 origin: OriginFor<T>,
749 asset_id: T::AssetId,
750 fee: Permill,
751 ) -> DispatchResult {
752 let level = T::ManagerOrigin::ensure_origin(origin)?;
753 ensure!(level.can_set_fees(), Error::<T>::InsufficientPrivilege);
754 ensure!(ExternalAssets::<T>::contains_key(&asset_id), Error::<T>::AssetNotApproved);
755 let old_value = MintingFee::<T>::get(&asset_id);
756 MintingFee::<T>::insert(&asset_id, fee);
757 Self::deposit_event(Event::MintingFeeUpdated { asset_id, old_value, new_value: fee });
758 Ok(())
759 }
760
761 #[pallet::call_index(3)]
776 #[pallet::weight(T::WeightInfo::set_redemption_fee())]
777 pub fn set_redemption_fee(
778 origin: OriginFor<T>,
779 asset_id: T::AssetId,
780 fee: Permill,
781 ) -> DispatchResult {
782 let level = T::ManagerOrigin::ensure_origin(origin)?;
783 ensure!(level.can_set_fees(), Error::<T>::InsufficientPrivilege);
784 ensure!(ExternalAssets::<T>::contains_key(&asset_id), Error::<T>::AssetNotApproved);
785 let old_value = RedemptionFee::<T>::get(&asset_id);
786 RedemptionFee::<T>::insert(&asset_id, fee);
787 Self::deposit_event(Event::RedemptionFeeUpdated {
788 asset_id,
789 old_value,
790 new_value: fee,
791 });
792 Ok(())
793 }
794
795 #[pallet::call_index(4)]
805 #[pallet::weight(T::WeightInfo::set_max_psm_debt())]
806 pub fn set_max_psm_debt(origin: OriginFor<T>, ratio: Permill) -> DispatchResult {
807 let level = T::ManagerOrigin::ensure_origin(origin)?;
808 ensure!(level.can_set_max_psm_debt(), Error::<T>::InsufficientPrivilege);
809 let old_value = MaxPsmDebtOfTotal::<T>::get();
810 MaxPsmDebtOfTotal::<T>::put(ratio);
811 Self::deposit_event(Event::MaxPsmDebtOfTotalUpdated { old_value, new_value: ratio });
812 Ok(())
813 }
814
815 #[pallet::call_index(5)]
842 #[pallet::weight(T::WeightInfo::set_asset_status())]
843 pub fn set_asset_status(
844 origin: OriginFor<T>,
845 asset_id: T::AssetId,
846 status: CircuitBreakerLevel,
847 ) -> DispatchResult {
848 T::ManagerOrigin::ensure_origin(origin)?;
849 ensure!(ExternalAssets::<T>::contains_key(&asset_id), Error::<T>::AssetNotApproved);
850 ExternalAssets::<T>::insert(&asset_id, status);
851 Self::deposit_event(Event::AssetStatusUpdated { asset_id, status });
852 Ok(())
853 }
854
855 #[pallet::call_index(6)]
878 #[pallet::weight(T::WeightInfo::set_asset_ceiling_weight())]
879 pub fn set_asset_ceiling_weight(
880 origin: OriginFor<T>,
881 asset_id: T::AssetId,
882 weight: Permill,
883 ) -> DispatchResult {
884 let level = T::ManagerOrigin::ensure_origin(origin)?;
885 ensure!(level.can_set_asset_ceiling(), Error::<T>::InsufficientPrivilege);
886 ensure!(ExternalAssets::<T>::contains_key(&asset_id), Error::<T>::AssetNotApproved);
887 let old_value = AssetCeilingWeight::<T>::get(&asset_id);
888 AssetCeilingWeight::<T>::insert(&asset_id, weight);
889 Self::deposit_event(Event::AssetCeilingWeightUpdated {
890 asset_id,
891 old_value,
892 new_value: weight,
893 });
894 Ok(())
895 }
896
897 #[pallet::call_index(7)]
915 #[pallet::weight(T::WeightInfo::add_external_asset())]
916 pub fn add_external_asset(origin: OriginFor<T>, asset_id: T::AssetId) -> DispatchResult {
917 let level = T::ManagerOrigin::ensure_origin(origin)?;
918 ensure!(level.can_manage_assets(), Error::<T>::InsufficientPrivilege);
919 ensure!(
920 !ExternalAssets::<T>::contains_key(&asset_id),
921 Error::<T>::AssetAlreadyApproved
922 );
923 ensure!(T::Fungibles::asset_exists(asset_id.clone()), Error::<T>::AssetDoesNotExist);
924 let count = ExternalAssets::<T>::count();
925 ensure!(count < T::MaxExternalAssets::get(), Error::<T>::TooManyAssets);
926
927 let asset_decimals = T::Fungibles::decimals(asset_id.clone());
928 let internal_decimals = InternalDecimals::<T>::get().ok_or(Error::<T>::Unexpected)?;
929 ensure!(
930 T::InternalAsset::decimals() == internal_decimals,
931 Error::<T>::DecimalsMismatch
932 );
933 ensure!(
934 (asset_decimals.abs_diff(internal_decimals) as u32) <= MAX_DECIMALS_DIFF,
935 Error::<T>::DecimalsRangeExceeded
936 );
937
938 ExternalAssets::<T>::insert(&asset_id, CircuitBreakerLevel::AllEnabled);
939 ExternalDecimals::<T>::insert(&asset_id, asset_decimals);
940 Self::deposit_event(Event::ExternalAssetAdded { asset_id });
941 Ok(())
942 }
943
944 #[pallet::call_index(8)]
973 #[pallet::weight(T::WeightInfo::remove_external_asset())]
974 pub fn remove_external_asset(origin: OriginFor<T>, asset_id: T::AssetId) -> DispatchResult {
975 let level = T::ManagerOrigin::ensure_origin(origin)?;
976 ensure!(level.can_manage_assets(), Error::<T>::InsufficientPrivilege);
977 ensure!(ExternalAssets::<T>::contains_key(&asset_id), Error::<T>::AssetNotApproved);
978 ensure!(PsmDebt::<T>::get(&asset_id).is_zero(), Error::<T>::AssetHasDebt);
979 ExternalAssets::<T>::remove(&asset_id);
980
981 MintingFee::<T>::remove(&asset_id);
983 RedemptionFee::<T>::remove(&asset_id);
984 AssetCeilingWeight::<T>::remove(&asset_id);
985 ExternalDecimals::<T>::remove(&asset_id);
986 PsmDebt::<T>::remove(&asset_id);
987 Self::deposit_event(Event::ExternalAssetRemoved { asset_id });
988 Ok(())
989 }
990 }
991
992 impl<T: Config> Pallet<T> {
993 pub(crate) fn account_id() -> T::AccountId {
995 T::PalletId::get().into_account_truncating()
996 }
997
998 pub(crate) fn max_psm_debt() -> BalanceOf<T> {
1000 let max_issuance = T::MaximumIssuance::get();
1001 MaxPsmDebtOfTotal::<T>::get().mul_floor(max_issuance)
1002 }
1003
1004 pub(crate) fn max_asset_debt(asset_id: T::AssetId) -> BalanceOf<T> {
1013 let asset_weight = AssetCeilingWeight::<T>::get(asset_id);
1014
1015 if asset_weight.is_zero() {
1016 return BalanceOf::<T>::zero();
1017 }
1018
1019 let total_weight_sum: u32 = AssetCeilingWeight::<T>::iter_values()
1020 .map(|w| w.deconstruct())
1021 .fold(0u32, |acc, x| acc.saturating_add(x));
1022
1023 if total_weight_sum == 0 {
1024 return BalanceOf::<T>::zero();
1025 }
1026
1027 let total_psm_ceiling = Self::max_psm_debt();
1028 Perbill::from_rational(asset_weight.deconstruct(), total_weight_sum)
1029 .mul_floor(total_psm_ceiling)
1030 }
1031
1032 pub(crate) fn total_psm_debt() -> BalanceOf<T> {
1034 PsmDebt::<T>::iter_values()
1035 .fold(BalanceOf::<T>::zero(), |acc, debt| acc.saturating_add(debt))
1036 }
1037
1038 #[cfg(test)]
1040 pub(crate) fn is_approved_asset(asset_id: &T::AssetId) -> bool {
1041 ExternalAssets::<T>::contains_key(asset_id)
1042 }
1043
1044 pub(crate) fn get_reserve(asset_id: T::AssetId) -> BalanceOf<T> {
1046 T::Fungibles::balance(asset_id, &Self::account_id())
1047 }
1048
1049 pub(crate) fn external_to_internal(
1055 amount: BalanceOf<T>,
1056 ext_decimals: u8,
1057 internal_decimals: u8,
1058 ) -> Result<BalanceOf<T>, Error<T>> {
1059 use core::cmp::Ordering::*;
1060 match ext_decimals.cmp(&internal_decimals) {
1061 Equal => Ok(amount),
1062 Less => {
1063 let diff = (internal_decimals - ext_decimals) as u32;
1064 let factor = Self::pow10(diff)?;
1065 amount.checked_mul(&factor).ok_or(Error::<T>::ConversionOverflow)
1066 },
1067 Greater => {
1068 let diff = (ext_decimals - internal_decimals) as u32;
1069 let factor = Self::pow10(diff)?;
1070 Ok(amount.checked_div(&factor).unwrap_or_else(BalanceOf::<T>::zero))
1071 },
1072 }
1073 }
1074
1075 pub(crate) fn internal_to_external(
1080 amount: BalanceOf<T>,
1081 ext_decimals: u8,
1082 internal_decimals: u8,
1083 ) -> Result<BalanceOf<T>, Error<T>> {
1084 use core::cmp::Ordering::*;
1085 match ext_decimals.cmp(&internal_decimals) {
1086 Equal => Ok(amount),
1087 Less => {
1088 let diff = (internal_decimals - ext_decimals) as u32;
1089 let factor = Self::pow10(diff)?;
1090 Ok(amount.checked_div(&factor).unwrap_or_else(BalanceOf::<T>::zero))
1091 },
1092 Greater => {
1093 let diff = (ext_decimals - internal_decimals) as u32;
1094 let factor = Self::pow10(diff)?;
1095 amount.checked_mul(&factor).ok_or(Error::<T>::ConversionOverflow)
1096 },
1097 }
1098 }
1099
1100 fn pow10(exp: u32) -> Result<BalanceOf<T>, Error<T>> {
1103 let factor_u128 = 10u128.checked_pow(exp).ok_or(Error::<T>::ConversionOverflow)?;
1104 factor_u128.try_into().map_err(|_| Error::<T>::ConversionOverflow)
1105 }
1106
1107 pub(crate) fn ensure_decimals_match(
1111 asset_id: T::AssetId,
1112 ) -> Result<(u8, u8), DispatchError> {
1113 let ext_decimals =
1114 ExternalDecimals::<T>::get(&asset_id).ok_or(Error::<T>::UnsupportedAsset)?;
1115 ensure!(T::Fungibles::decimals(asset_id) == ext_decimals, Error::<T>::DecimalsMismatch);
1116
1117 let internal_decimals = InternalDecimals::<T>::get().ok_or(Error::<T>::Unexpected)?;
1118 ensure!(
1119 T::InternalAsset::decimals() == internal_decimals,
1120 Error::<T>::DecimalsMismatch
1121 );
1122
1123 Ok((ext_decimals, internal_decimals))
1124 }
1125
1126 pub(crate) fn ensure_account_exists(account: &T::AccountId) {
1128 if !frame_system::Pallet::<T>::account_exists(account) {
1129 frame_system::Pallet::<T>::inc_providers(account);
1130 }
1131 }
1132
1133 #[cfg(any(feature = "try-runtime", test))]
1134 pub(crate) fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
1135 use sp_runtime::traits::CheckedAdd;
1136
1137 let internal_decimals_snapshot =
1140 InternalDecimals::<T>::get().ok_or("InternalDecimals not initialized")?;
1141 ensure!(
1142 T::InternalAsset::decimals() == internal_decimals_snapshot,
1143 "Internal asset live decimals differ from the genesis snapshot"
1144 );
1145 for (asset_id, _) in ExternalAssets::<T>::iter() {
1146 let snapshot = ExternalDecimals::<T>::get(&asset_id)
1147 .ok_or("Approved external asset missing decimals snapshot")?;
1148 ensure!(
1149 T::Fungibles::decimals(asset_id) == snapshot,
1150 "External asset live decimals differ from the registration snapshot"
1151 );
1152 }
1153
1154 for (asset_id, _) in ExternalAssets::<T>::iter() {
1157 let debt = PsmDebt::<T>::get(&asset_id);
1158 let reserve = Self::get_reserve(asset_id.clone());
1159 let ext_decimals = ExternalDecimals::<T>::get(&asset_id)
1160 .ok_or("Approved external asset missing decimals snapshot")?;
1161 let debt_as_external =
1162 Self::internal_to_external(debt, ext_decimals, internal_decimals_snapshot)
1163 .map_err(|_| "Failed to convert tracked debt to external units")?;
1164 ensure!(
1165 reserve >= debt_as_external,
1166 "PSM reserve is less than tracked debt for an asset"
1167 );
1168 }
1169
1170 let mut sum = BalanceOf::<T>::zero();
1172 for (asset_id, _) in ExternalAssets::<T>::iter() {
1173 sum = sum
1174 .checked_add(&PsmDebt::<T>::get(&asset_id))
1175 .ok_or("PSM debt overflow when summing per-asset debts")?;
1176 }
1177 ensure!(
1178 Self::total_psm_debt() == sum,
1179 "total_psm_debt() does not match sum of per-asset debts"
1180 );
1181
1182 for (asset_id, status) in ExternalAssets::<T>::iter() {
1186 if status.allows_minting() {
1187 let debt = PsmDebt::<T>::get(&asset_id);
1188 let ceiling = Self::max_asset_debt(asset_id);
1189 ensure!(debt <= ceiling, "Per-asset PSM debt exceeds its ceiling");
1190 }
1191 }
1192
1193 Ok(())
1194 }
1195 }
1196}
1197
1198impl<T: pallet::Config> PsmInterface for pallet::Pallet<T> {
1199 type Balance = pallet::BalanceOf<T>;
1200
1201 fn reserved_capacity() -> Self::Balance {
1202 Self::max_psm_debt()
1203 }
1204}