1#![cfg_attr(not(feature = "std"), no_std)]
99
100extern crate alloc;
101
102pub mod weights;
103
104#[cfg(feature = "runtime-benchmarks")]
105mod benchmarking;
106#[cfg(test)]
107mod mock;
108#[cfg(test)]
109mod tests;
110
111pub use pallet::*;
112pub use weights::WeightInfo;
113
114#[cfg(feature = "runtime-benchmarks")]
120pub trait BenchmarkHelper<AssetId, AccountId> {
121 fn get_asset_id(asset_index: u32) -> AssetId;
123 fn create_asset(asset_id: AssetId, owner: &AccountId, decimals: u8);
125}
126
127#[frame_support::pallet]
128pub mod pallet {
129
130 use alloc::boxed::Box;
131 use codec::DecodeWithMemTracking;
132 use frame_support::{
133 pallet_prelude::*,
134 traits::{
135 fungibles::{
136 metadata::Inspect as FungiblesMetadataInspect,
137 roles::Inspect as FungiblesRolesInspect, Inspect as FungiblesInspect,
138 Mutate as FungiblesMutate,
139 },
140 tokens::{Fortitude, Precision, Preservation},
141 CallerTrait, Consideration, EnsureOriginWithArg, Footprint, OriginTrait,
142 },
143 PalletId,
144 };
145 use frame_system::pallet_prelude::*;
146 use sp_runtime::{
147 traits::{CheckedDiv, CheckedMul, Saturating, TrailingZeroInput, Zero},
148 Perbill, Permill, TypeId,
149 };
150
151 use crate::WeightInfo;
152
153 #[derive(
155 Encode,
156 Decode,
157 DecodeWithMemTracking,
158 MaxEncodedLen,
159 TypeInfo,
160 Clone,
161 Copy,
162 PartialEq,
163 Eq,
164 Debug,
165 Default,
166 )]
167 pub enum CircuitBreakerLevel {
168 #[default]
170 AllEnabled,
171 MintingDisabled,
173 AllDisabled,
175 }
176
177 impl CircuitBreakerLevel {
178 pub const fn allows_minting(&self) -> bool {
180 matches!(self, CircuitBreakerLevel::AllEnabled)
181 }
182
183 pub const fn allows_redemption(&self) -> bool {
185 !matches!(self, CircuitBreakerLevel::AllDisabled)
186 }
187 }
188
189 #[derive(
195 Encode,
196 Decode,
197 DecodeWithMemTracking,
198 MaxEncodedLen,
199 TypeInfo,
200 Clone,
201 Copy,
202 PartialEq,
203 Eq,
204 Debug,
205 Default,
206 )]
207 pub enum PsmManagerLevel {
208 #[default]
212 Full,
213 Emergency,
216 }
217
218 impl PsmManagerLevel {
219 pub const fn can_set_fees(&self) -> bool {
221 matches!(self, PsmManagerLevel::Full)
222 }
223
224 pub const fn can_set_circuit_breaker(&self) -> bool {
227 matches!(self, PsmManagerLevel::Full | PsmManagerLevel::Emergency)
228 }
229
230 pub const fn can_set_max_debt(&self) -> bool {
233 matches!(self, PsmManagerLevel::Full)
234 }
235
236 pub const fn can_set_asset_ceiling(&self) -> bool {
239 matches!(self, PsmManagerLevel::Full)
240 }
241
242 pub const fn can_manage_assets(&self) -> bool {
244 matches!(self, PsmManagerLevel::Full)
245 }
246
247 pub const fn can_manage_admins(&self) -> bool {
249 matches!(self, PsmManagerLevel::Full)
250 }
251
252 pub const fn can_remove_psm(&self) -> bool {
254 matches!(self, PsmManagerLevel::Full)
255 }
256 }
257
258 pub(crate) type BalanceOf<T> = <<T as Config>::Fungibles as FungiblesInspect<
259 <T as frame_system::Config>::AccountId,
260 >>::Balance;
261
262 pub(crate) struct DefaultFee;
264 impl Get<Permill> for DefaultFee {
265 fn get() -> Permill {
266 Permill::from_parts(5_000)
267 }
268 }
269
270 pub const MAX_DECIMALS_DIFF: u32 = 24;
274
275 #[derive(
277 Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, Clone, PartialEq, Eq, Debug,
278 )]
279 #[scale_info(skip_type_params(T))]
280 pub struct PsmInfo<T: Config> {
281 pub fee_destination: T::AccountId,
283 pub max_debt: BalanceOf<T>,
285 pub min_swap_amount: BalanceOf<T>,
288 pub internal_decimals: u8,
290 pub external_count: u32,
292 }
293
294 #[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, PartialEq, Eq, Debug)]
297 #[scale_info(skip_type_params(T))]
298 pub struct PsmAdminInfo<T: Config> {
301 pub full_admin: T::PalletsOrigin,
304 pub emergency_admin: T::PalletsOrigin,
307 pub deposit: Option<(T::AccountId, T::Consideration)>,
310 }
311
312 #[derive(
314 Encode,
315 Decode,
316 DecodeWithMemTracking,
317 MaxEncodedLen,
318 TypeInfo,
319 Clone,
320 Copy,
321 PartialEq,
322 Eq,
323 Debug,
324 )]
325 pub struct ExternalAssetInfo {
326 pub status: CircuitBreakerLevel,
328 pub decimals: u8,
330 }
331
332 #[pallet::config]
333 pub trait Config: frame_system::Config {
334 type Fungibles: FungiblesMutate<Self::AccountId, AssetId = Self::AssetId>
336 + FungiblesMetadataInspect<Self::AccountId>
337 + FungiblesRolesInspect<Self::AccountId>;
338
339 type Consideration: Consideration<Self::AccountId, Footprint>;
342
343 type CreateOrigin: EnsureOriginWithArg<
347 <Self as frame_system::Config>::RuntimeOrigin,
348 Self::AssetId,
349 Success = Option<Self::AccountId>,
350 >;
351
352 type RuntimeOrigin: OriginTrait<PalletsOrigin = Self::PalletsOrigin>
355 + From<Self::PalletsOrigin>
356 + IsType<<Self as frame_system::Config>::RuntimeOrigin>;
357
358 type PalletsOrigin: Parameter
361 + From<frame_system::RawOrigin<Self::AccountId>>
362 + CallerTrait<Self::AccountId>
363 + MaxEncodedLen;
364
365 type AssetId: Parameter + Member + Clone + MaybeSerializeDeserialize + MaxEncodedLen + Ord;
367
368 type WeightInfo: WeightInfo;
370
371 #[pallet::constant]
373 type PalletId: Get<PalletId>;
374
375 #[pallet::constant]
377 type MaxExternals: Get<u32>;
378
379 #[cfg(feature = "runtime-benchmarks")]
381 type BenchmarkHelper: crate::BenchmarkHelper<Self::AssetId, Self::AccountId>;
382 }
383
384 pub struct EnsureAssetOwner<T>(core::marker::PhantomData<T>);
388
389 impl<T: Config> EnsureOriginWithArg<<T as frame_system::Config>::RuntimeOrigin, T::AssetId>
390 for EnsureAssetOwner<T>
391 {
392 type Success = Option<T::AccountId>;
393
394 fn try_origin(
395 origin: <T as frame_system::Config>::RuntimeOrigin,
396 internal_asset: &T::AssetId,
397 ) -> Result<Self::Success, <T as frame_system::Config>::RuntimeOrigin> {
398 match ensure_signed(origin.clone()) {
399 Ok(who) if T::Fungibles::owner(internal_asset.clone()) == Some(who.clone()) => {
400 Ok(Some(who))
401 },
402 _ => Err(origin),
403 }
404 }
405
406 #[cfg(feature = "runtime-benchmarks")]
407 fn try_successful_origin(
408 internal_asset: &T::AssetId,
409 ) -> Result<<T as frame_system::Config>::RuntimeOrigin, ()> {
410 let owner = T::Fungibles::owner(internal_asset.clone()).ok_or(())?;
412 Ok(frame_system::RawOrigin::Signed(owner).into())
413 }
414 }
415
416 const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
418
419 #[pallet::pallet]
420 #[pallet::storage_version(STORAGE_VERSION)]
421 pub struct Pallet<T>(_);
422
423 #[pallet::composite_enum]
425 pub enum HoldReason {
426 #[codec(index = 0)]
428 CreationDeposit,
429 }
430
431 #[pallet::hooks]
432 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
433 fn integrity_test() {
434 const ENTROPY_BYTES: usize = 32;
435 assert!(
436 <T::AccountId as MaxEncodedLen>::max_encoded_len() >= ENTROPY_BYTES,
437 "T::AccountId is too small to preserve the full PSM reserve-account entropy",
438 );
439 }
440
441 #[cfg(feature = "try-runtime")]
442 fn try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
443 Self::do_try_state()
444 }
445 }
446
447 #[pallet::storage]
449 pub type Psm<T: Config> = StorageMap<_, Blake2_128Concat, T::AssetId, PsmInfo<T>, OptionQuery>;
450
451 #[pallet::storage]
455 pub type PsmAdmin<T: Config> =
456 StorageMap<_, Blake2_128Concat, T::AssetId, PsmAdminInfo<T>, OptionQuery>;
457
458 #[pallet::storage]
460 pub type PsmDebt<T: Config> = StorageDoubleMap<
461 _,
462 Blake2_128Concat,
463 T::AssetId,
464 Blake2_128Concat,
465 T::AssetId,
466 BalanceOf<T>,
467 ValueQuery,
468 >;
469
470 #[pallet::storage]
473 pub(crate) type MintingFee<T: Config> = StorageDoubleMap<
474 _,
475 Blake2_128Concat,
476 T::AssetId,
477 Blake2_128Concat,
478 T::AssetId,
479 Permill,
480 ValueQuery,
481 DefaultFee,
482 >;
483
484 #[pallet::storage]
487 pub(crate) type RedemptionFee<T: Config> = StorageDoubleMap<
488 _,
489 Blake2_128Concat,
490 T::AssetId,
491 Blake2_128Concat,
492 T::AssetId,
493 Permill,
494 ValueQuery,
495 DefaultFee,
496 >;
497
498 #[pallet::storage]
501 pub(crate) type AssetCeilingWeight<T: Config> = StorageDoubleMap<
502 _,
503 Blake2_128Concat,
504 T::AssetId,
505 Blake2_128Concat,
506 T::AssetId,
507 Permill,
508 ValueQuery,
509 >;
510
511 #[pallet::storage]
513 pub(crate) type ExternalAssets<T: Config> = StorageDoubleMap<
514 _,
515 Blake2_128Concat,
516 T::AssetId,
517 Blake2_128Concat,
518 T::AssetId,
519 ExternalAssetInfo,
520 OptionQuery,
521 >;
522
523 #[pallet::event]
524 #[pallet::generate_deposit(pub(super) fn deposit_event)]
525 pub enum Event<T: Config> {
526 Minted {
528 who: T::AccountId,
529 internal_asset: T::AssetId,
530 external_asset: T::AssetId,
531 external_consumed: BalanceOf<T>,
532 internal_received: BalanceOf<T>,
533 internal_fee: BalanceOf<T>,
534 },
535 Redeemed {
537 who: T::AccountId,
538 internal_asset: T::AssetId,
539 external_asset: T::AssetId,
540 internal_consumed: BalanceOf<T>,
541 external_received: BalanceOf<T>,
542 internal_fee: BalanceOf<T>,
543 },
544 MintingFeeUpdated {
546 internal_asset: T::AssetId,
547 external_asset: T::AssetId,
548 old_value: Permill,
549 new_value: Permill,
550 },
551 RedemptionFeeUpdated {
553 internal_asset: T::AssetId,
554 external_asset: T::AssetId,
555 old_value: Permill,
556 new_value: Permill,
557 },
558 MaxDebtUpdated {
560 internal_asset: T::AssetId,
561 old_value: BalanceOf<T>,
562 new_value: BalanceOf<T>,
563 },
564 AssetCeilingWeightUpdated {
566 internal_asset: T::AssetId,
567 external_asset: T::AssetId,
568 old_value: Permill,
569 new_value: Permill,
570 },
571 AssetStatusUpdated {
573 internal_asset: T::AssetId,
574 external_asset: T::AssetId,
575 status: CircuitBreakerLevel,
576 },
577 ExternalAssetAdded { internal_asset: T::AssetId, external_asset: T::AssetId },
579 ExternalAssetRemoved { internal_asset: T::AssetId, external_asset: T::AssetId },
581 PsmCreated {
583 internal_asset: T::AssetId,
584 full_admin: Box<T::PalletsOrigin>,
586 emergency_admin: Box<T::PalletsOrigin>,
587 fee_destination: T::AccountId,
588 max_debt: BalanceOf<T>,
589 },
590 PsmRemoved { internal_asset: T::AssetId },
592 FullAdminChanged {
594 internal_asset: T::AssetId,
595 old_admin: Box<T::PalletsOrigin>,
596 new_admin: Box<T::PalletsOrigin>,
597 },
598 EmergencyAdminChanged {
600 internal_asset: T::AssetId,
601 old_admin: Box<T::PalletsOrigin>,
602 new_admin: Box<T::PalletsOrigin>,
603 },
604 }
605
606 #[pallet::error]
607 pub enum Error<T> {
608 InsufficientReserve,
610 ExceedsMaxPsmDebt,
612 BelowMinimumSwap,
614 FeeTooHigh,
616 ZeroMinSwapAmount,
618 MintingStopped,
620 AllSwapsStopped,
622 UnsupportedAsset,
624 PsmNotFound,
626 AssetAlreadyApproved,
628 AssetDoesNotExist,
630 AssetNotApproved,
632 AssetHasDebt,
634 InsufficientPrivilege,
637 TooManyAssets,
639 DecimalsMismatch,
641 DecimalsRangeExceeded,
643 ConversionOverflow,
645 AmountTooSmallAfterConversion,
647 PsmAlreadyExists,
649 PsmHasDebt,
651 PsmHasApprovedExternals,
653 Unexpected,
655 }
656
657 #[pallet::call]
658 impl<T: Config> Pallet<T> {
659 #[pallet::call_index(0)]
701 #[pallet::weight(T::WeightInfo::mint(T::MaxExternals::get()))]
702 pub fn mint(
703 origin: OriginFor<T>,
704 internal_asset: T::AssetId,
705 external_asset: T::AssetId,
706 external_amount: BalanceOf<T>,
707 max_fee: Permill,
708 ) -> DispatchResult {
709 let who = ensure_signed(origin)?;
710 let info = Psm::<T>::get(&internal_asset).ok_or(Error::<T>::PsmNotFound)?;
711
712 let external = ExternalAssets::<T>::get(&internal_asset, &external_asset)
713 .ok_or(Error::<T>::UnsupportedAsset)?;
714 ensure!(external.status.allows_minting(), Error::<T>::MintingStopped);
715
716 let (ext_decimals, internal_decimals) =
717 Self::ensure_decimals_match(&info, &internal_asset, &external_asset, &external)?;
718
719 let internal_equivalent =
720 Self::external_to_internal(external_amount, ext_decimals, internal_decimals)?;
721 ensure!(!internal_equivalent.is_zero(), Error::<T>::AmountTooSmallAfterConversion);
722 ensure!(internal_equivalent >= info.min_swap_amount, Error::<T>::BelowMinimumSwap);
723
724 let effective_external =
725 Self::internal_to_external(internal_equivalent, ext_decimals, internal_decimals)?;
726
727 let fee_rate = MintingFee::<T>::get(&internal_asset, &external_asset);
728 ensure!(fee_rate <= max_fee, Error::<T>::FeeTooHigh);
729 let fee = fee_rate.mul_ceil(internal_equivalent);
730 let internal_to_user = internal_equivalent.saturating_sub(fee);
731
732 let current_total_psm_debt = Self::total_psm_debt(&internal_asset);
733 ensure!(
734 current_total_psm_debt.saturating_add(internal_equivalent) <= info.max_debt,
735 Error::<T>::ExceedsMaxPsmDebt
736 );
737
738 let current_debt = PsmDebt::<T>::get(&internal_asset, &external_asset);
739 let max_debt = Self::max_asset_debt(&internal_asset, &external_asset, &info);
740 let new_debt = current_debt.saturating_add(internal_equivalent);
741 ensure!(new_debt <= max_debt, Error::<T>::ExceedsMaxPsmDebt);
742
743 let psm_account = Self::psm_account(&internal_asset);
744 T::Fungibles::transfer(
745 external_asset.clone(),
746 &who,
747 &psm_account,
748 effective_external,
749 Preservation::Expendable,
750 )?;
751 T::Fungibles::mint_into(internal_asset.clone(), &who, internal_to_user)?;
752 if !fee.is_zero() {
753 T::Fungibles::mint_into(internal_asset.clone(), &info.fee_destination, fee)?;
754 }
755
756 PsmDebt::<T>::insert(&internal_asset, &external_asset, new_debt);
757
758 Self::deposit_event(Event::Minted {
759 who,
760 internal_asset,
761 external_asset,
762 external_consumed: effective_external,
763 internal_received: internal_to_user,
764 internal_fee: fee,
765 });
766 Ok(())
767 }
768
769 #[pallet::call_index(1)]
810 #[pallet::weight(T::WeightInfo::redeem())]
811 pub fn redeem(
812 origin: OriginFor<T>,
813 internal_asset: T::AssetId,
814 external_asset: T::AssetId,
815 internal_amount: BalanceOf<T>,
816 max_fee: Permill,
817 ) -> DispatchResult {
818 let who = ensure_signed(origin)?;
819 let info = Psm::<T>::get(&internal_asset).ok_or(Error::<T>::PsmNotFound)?;
820
821 let external = ExternalAssets::<T>::get(&internal_asset, &external_asset)
822 .ok_or(Error::<T>::UnsupportedAsset)?;
823 ensure!(external.status.allows_redemption(), Error::<T>::AllSwapsStopped);
824
825 let ext_decimals = external.decimals;
826 let internal_decimals = info.internal_decimals;
827
828 ensure!(internal_amount >= info.min_swap_amount, Error::<T>::BelowMinimumSwap);
829
830 let fee_rate = RedemptionFee::<T>::get(&internal_asset, &external_asset);
831 ensure!(fee_rate <= max_fee, Error::<T>::FeeTooHigh);
832 let fee = fee_rate.mul_ceil(internal_amount);
833 let internal_net = internal_amount.saturating_sub(fee);
834
835 let external_out =
836 Self::internal_to_external(internal_net, ext_decimals, internal_decimals)?;
837 ensure!(
838 internal_net.is_zero() || !external_out.is_zero(),
839 Error::<T>::AmountTooSmallAfterConversion
840 );
841 let effective_internal_net =
846 Self::external_to_internal(external_out, ext_decimals, internal_decimals)?;
847
848 let current_debt = PsmDebt::<T>::get(&internal_asset, &external_asset);
849 ensure!(current_debt >= effective_internal_net, Error::<T>::InsufficientReserve);
850
851 let reserve = Self::get_reserve(&internal_asset, &external_asset);
852 if reserve < external_out {
853 defensive!("PSM reserve is less than expected output amount");
854 return Err(Error::<T>::Unexpected.into());
855 }
856
857 if !fee.is_zero() {
858 T::Fungibles::transfer(
859 internal_asset.clone(),
860 &who,
861 &info.fee_destination,
862 fee,
863 Preservation::Expendable,
864 )?;
865 }
866
867 if !effective_internal_net.is_zero() {
868 T::Fungibles::burn_from(
869 internal_asset.clone(),
870 &who,
871 effective_internal_net,
872 Preservation::Expendable,
873 Precision::Exact,
874 Fortitude::Polite,
875 )?;
876 }
877
878 let psm_account = Self::psm_account(&internal_asset);
879 if !external_out.is_zero() {
880 T::Fungibles::transfer(
881 external_asset.clone(),
882 &psm_account,
883 &who,
884 external_out,
885 Preservation::Expendable,
886 )?;
887 }
888
889 PsmDebt::<T>::mutate(&internal_asset, &external_asset, |debt| {
890 *debt = debt.saturating_sub(effective_internal_net);
891 });
892
893 Self::deposit_event(Event::Redeemed {
894 who,
895 internal_asset,
896 external_asset,
897 internal_consumed: effective_internal_net.saturating_add(fee),
898 external_received: external_out,
899 internal_fee: fee,
900 });
901 Ok(())
902 }
903
904 #[pallet::call_index(2)]
941 #[pallet::weight(T::WeightInfo::create_psm())]
942 pub fn create_psm(
943 origin: OriginFor<T>,
944 internal_asset: T::AssetId,
945 full_admin: Box<T::PalletsOrigin>,
946 emergency_admin: Box<T::PalletsOrigin>,
947 fee_destination: T::AccountId,
948 max_debt: BalanceOf<T>,
949 min_swap_amount: BalanceOf<T>,
950 ) -> DispatchResult {
951 let maybe_depositor = T::CreateOrigin::ensure_origin(origin, &internal_asset)?;
952 ensure!(!Psm::<T>::contains_key(&internal_asset), Error::<T>::PsmAlreadyExists);
953 ensure!(!min_swap_amount.is_zero(), Error::<T>::ZeroMinSwapAmount);
954 ensure!(
955 T::Fungibles::asset_exists(internal_asset.clone()),
956 Error::<T>::AssetDoesNotExist
957 );
958
959 let deposit = maybe_depositor
960 .map(|depositor| {
961 T::Consideration::new(&depositor, Footprint::from_parts(1, 0))
962 .map(|ticket| (depositor, ticket))
963 })
964 .transpose()?;
965
966 let full_admin = *full_admin;
967 let emergency_admin = *emergency_admin;
968 let internal_decimals = T::Fungibles::decimals(internal_asset.clone());
969 Psm::<T>::insert(
970 &internal_asset,
971 PsmInfo::<T> {
972 fee_destination: fee_destination.clone(),
973 max_debt,
974 min_swap_amount,
975 internal_decimals,
976 external_count: 0,
977 },
978 );
979 PsmAdmin::<T>::insert(
980 &internal_asset,
981 PsmAdminInfo::<T> {
982 full_admin: full_admin.clone(),
983 emergency_admin: emergency_admin.clone(),
984 deposit,
985 },
986 );
987 frame_system::Pallet::<T>::inc_providers(&Self::psm_account(&internal_asset));
993 frame_system::Pallet::<T>::inc_providers(&fee_destination);
994
995 Self::deposit_event(Event::PsmCreated {
996 internal_asset,
997 full_admin: Box::new(full_admin),
998 emergency_admin: Box::new(emergency_admin),
999 fee_destination,
1000 max_debt,
1001 });
1002 Ok(())
1003 }
1004
1005 #[pallet::call_index(3)]
1029 #[pallet::weight(T::WeightInfo::remove_psm())]
1030 pub fn remove_psm(origin: OriginFor<T>, internal_asset: T::AssetId) -> DispatchResult {
1031 Self::ensure_psm_admin(origin, &internal_asset, |l| l.can_remove_psm())?;
1032 let info = Psm::<T>::get(&internal_asset).ok_or(Error::<T>::PsmNotFound)?;
1033 ensure!(info.external_count == 0, Error::<T>::PsmHasApprovedExternals);
1034 ensure!(Self::total_psm_debt(&internal_asset).is_zero(), Error::<T>::PsmHasDebt);
1035
1036 let PsmAdminInfo { deposit, .. } =
1037 PsmAdmin::<T>::get(&internal_asset).ok_or(Error::<T>::PsmNotFound)?;
1038 if let Some((depositor, ticket)) = deposit {
1039 ticket.drop(&depositor)?;
1040 }
1041
1042 Psm::<T>::remove(&internal_asset);
1043 PsmAdmin::<T>::remove(&internal_asset);
1044
1045 frame_system::Pallet::<T>::dec_providers(&Self::psm_account(&internal_asset)).ok();
1049 frame_system::Pallet::<T>::dec_providers(&info.fee_destination).ok();
1050
1051 Self::deposit_event(Event::PsmRemoved { internal_asset });
1052 Ok(())
1053 }
1054
1055 #[pallet::call_index(4)]
1076 #[pallet::weight(T::WeightInfo::set_minting_fee())]
1077 pub fn set_minting_fee(
1078 origin: OriginFor<T>,
1079 internal_asset: T::AssetId,
1080 external_asset: T::AssetId,
1081 fee: Permill,
1082 ) -> DispatchResult {
1083 Self::ensure_psm_admin(origin, &internal_asset, |l| l.can_set_fees())?;
1084 ensure!(
1085 ExternalAssets::<T>::contains_key(&internal_asset, &external_asset),
1086 Error::<T>::AssetNotApproved
1087 );
1088 let old_value = MintingFee::<T>::get(&internal_asset, &external_asset);
1089 MintingFee::<T>::insert(&internal_asset, &external_asset, fee);
1090 Self::deposit_event(Event::MintingFeeUpdated {
1091 internal_asset,
1092 external_asset,
1093 old_value,
1094 new_value: fee,
1095 });
1096 Ok(())
1097 }
1098
1099 #[pallet::call_index(5)]
1120 #[pallet::weight(T::WeightInfo::set_redemption_fee())]
1121 pub fn set_redemption_fee(
1122 origin: OriginFor<T>,
1123 internal_asset: T::AssetId,
1124 external_asset: T::AssetId,
1125 fee: Permill,
1126 ) -> DispatchResult {
1127 Self::ensure_psm_admin(origin, &internal_asset, |l| l.can_set_fees())?;
1128 ensure!(
1129 ExternalAssets::<T>::contains_key(&internal_asset, &external_asset),
1130 Error::<T>::AssetNotApproved
1131 );
1132 let old_value = RedemptionFee::<T>::get(&internal_asset, &external_asset);
1133 RedemptionFee::<T>::insert(&internal_asset, &external_asset, fee);
1134 Self::deposit_event(Event::RedemptionFeeUpdated {
1135 internal_asset,
1136 external_asset,
1137 old_value,
1138 new_value: fee,
1139 });
1140 Ok(())
1141 }
1142
1143 #[pallet::call_index(6)]
1165 #[pallet::weight(T::WeightInfo::set_max_debt())]
1166 pub fn set_max_debt(
1167 origin: OriginFor<T>,
1168 internal_asset: T::AssetId,
1169 value: BalanceOf<T>,
1170 ) -> DispatchResult {
1171 Self::ensure_psm_admin(origin, &internal_asset, |l| l.can_set_max_debt())?;
1172 let old_value =
1176 Psm::<T>::try_mutate(&internal_asset, |maybe| -> Result<_, DispatchError> {
1177 let info = maybe.as_mut().ok_or(Error::<T>::PsmNotFound)?;
1178 Ok(core::mem::replace(&mut info.max_debt, value))
1179 })?;
1180 Self::deposit_event(Event::MaxDebtUpdated {
1181 internal_asset,
1182 old_value,
1183 new_value: value,
1184 });
1185 Ok(())
1186 }
1187
1188 #[pallet::call_index(7)]
1209 #[pallet::weight(T::WeightInfo::set_asset_status())]
1210 pub fn set_asset_status(
1211 origin: OriginFor<T>,
1212 internal_asset: T::AssetId,
1213 external_asset: T::AssetId,
1214 status: CircuitBreakerLevel,
1215 ) -> DispatchResult {
1216 Self::ensure_psm_admin(origin, &internal_asset, |l| l.can_set_circuit_breaker())?;
1217 ExternalAssets::<T>::try_mutate(
1218 &internal_asset,
1219 &external_asset,
1220 |maybe| -> DispatchResult {
1221 let info = maybe.as_mut().ok_or(Error::<T>::AssetNotApproved)?;
1222 info.status = status;
1223 Ok(())
1224 },
1225 )?;
1226 Self::deposit_event(Event::AssetStatusUpdated {
1227 internal_asset,
1228 external_asset,
1229 status,
1230 });
1231 Ok(())
1232 }
1233
1234 #[pallet::call_index(8)]
1259 #[pallet::weight(T::WeightInfo::set_asset_ceiling_weight())]
1260 pub fn set_asset_ceiling_weight(
1261 origin: OriginFor<T>,
1262 internal_asset: T::AssetId,
1263 external_asset: T::AssetId,
1264 weight: Permill,
1265 ) -> DispatchResult {
1266 Self::ensure_psm_admin(origin, &internal_asset, |l| l.can_set_asset_ceiling())?;
1267 ensure!(
1268 ExternalAssets::<T>::contains_key(&internal_asset, &external_asset),
1269 Error::<T>::AssetNotApproved
1270 );
1271 let old_value = AssetCeilingWeight::<T>::get(&internal_asset, &external_asset);
1274 AssetCeilingWeight::<T>::insert(&internal_asset, &external_asset, weight);
1275 Self::deposit_event(Event::AssetCeilingWeightUpdated {
1276 internal_asset,
1277 external_asset,
1278 old_value,
1279 new_value: weight,
1280 });
1281 Ok(())
1282 }
1283
1284 #[pallet::call_index(9)]
1315 #[pallet::weight(T::WeightInfo::add_external_asset())]
1316 pub fn add_external_asset(
1317 origin: OriginFor<T>,
1318 internal_asset: T::AssetId,
1319 external_asset: T::AssetId,
1320 ) -> DispatchResult {
1321 Self::ensure_psm_admin(origin, &internal_asset, |l| l.can_manage_assets())?;
1322 let mut info = Psm::<T>::get(&internal_asset).ok_or(Error::<T>::PsmNotFound)?;
1323 ensure!(
1324 !ExternalAssets::<T>::contains_key(&internal_asset, &external_asset),
1325 Error::<T>::AssetAlreadyApproved
1326 );
1327 ensure!(info.external_count < T::MaxExternals::get(), Error::<T>::TooManyAssets);
1328 ensure!(
1329 T::Fungibles::asset_exists(external_asset.clone()),
1330 Error::<T>::AssetDoesNotExist
1331 );
1332
1333 let asset_decimals = T::Fungibles::decimals(external_asset.clone());
1334 ensure!(
1335 T::Fungibles::decimals(internal_asset.clone()) == info.internal_decimals,
1336 Error::<T>::DecimalsMismatch
1337 );
1338 ensure!(
1339 (asset_decimals.abs_diff(info.internal_decimals) as u32) <= MAX_DECIMALS_DIFF,
1340 Error::<T>::DecimalsRangeExceeded
1341 );
1342
1343 ExternalAssets::<T>::insert(
1344 &internal_asset,
1345 &external_asset,
1346 ExternalAssetInfo {
1347 status: CircuitBreakerLevel::AllEnabled,
1348 decimals: asset_decimals,
1349 },
1350 );
1351 info.external_count = info.external_count.saturating_add(1);
1352 Psm::<T>::insert(&internal_asset, info);
1353
1354 Self::deposit_event(Event::ExternalAssetAdded { internal_asset, external_asset });
1355 Ok(())
1356 }
1357
1358 #[pallet::call_index(10)]
1384 #[pallet::weight(T::WeightInfo::remove_external_asset())]
1385 pub fn remove_external_asset(
1386 origin: OriginFor<T>,
1387 internal_asset: T::AssetId,
1388 external_asset: T::AssetId,
1389 ) -> DispatchResult {
1390 Self::ensure_psm_admin(origin, &internal_asset, |l| l.can_manage_assets())?;
1391 let mut info = Psm::<T>::get(&internal_asset).ok_or(Error::<T>::PsmNotFound)?;
1392 ensure!(
1393 ExternalAssets::<T>::contains_key(&internal_asset, &external_asset),
1394 Error::<T>::AssetNotApproved
1395 );
1396 ensure!(
1397 PsmDebt::<T>::get(&internal_asset, &external_asset).is_zero(),
1398 Error::<T>::AssetHasDebt
1399 );
1400 ExternalAssets::<T>::remove(&internal_asset, &external_asset);
1401 MintingFee::<T>::remove(&internal_asset, &external_asset);
1402 RedemptionFee::<T>::remove(&internal_asset, &external_asset);
1403 AssetCeilingWeight::<T>::remove(&internal_asset, &external_asset);
1404 PsmDebt::<T>::remove(&internal_asset, &external_asset);
1405 info.external_count = info.external_count.saturating_sub(1);
1406 Psm::<T>::insert(&internal_asset, info);
1407
1408 Self::deposit_event(Event::ExternalAssetRemoved { internal_asset, external_asset });
1409 Ok(())
1410 }
1411
1412 #[pallet::call_index(11)]
1431 #[pallet::weight(T::WeightInfo::set_full_admin())]
1432 pub fn set_full_admin(
1433 origin: OriginFor<T>,
1434 internal_asset: T::AssetId,
1435 new_admin: Box<T::PalletsOrigin>,
1436 ) -> DispatchResult {
1437 Self::ensure_psm_admin(origin, &internal_asset, |l| l.can_manage_admins())?;
1438 let new_admin = *new_admin;
1439 let old_admin = PsmAdmin::<T>::try_mutate(
1440 &internal_asset,
1441 |maybe| -> Result<T::PalletsOrigin, DispatchError> {
1442 let admin = maybe.as_mut().ok_or(Error::<T>::PsmNotFound)?;
1443 let old = core::mem::replace(&mut admin.full_admin, new_admin.clone());
1444 Ok(old)
1445 },
1446 )?;
1447 Self::deposit_event(Event::FullAdminChanged {
1448 internal_asset,
1449 old_admin: Box::new(old_admin),
1450 new_admin: Box::new(new_admin),
1451 });
1452 Ok(())
1453 }
1454
1455 #[pallet::call_index(12)]
1474 #[pallet::weight(T::WeightInfo::set_emergency_admin())]
1475 pub fn set_emergency_admin(
1476 origin: OriginFor<T>,
1477 internal_asset: T::AssetId,
1478 new_admin: Box<T::PalletsOrigin>,
1479 ) -> DispatchResult {
1480 Self::ensure_psm_admin(origin, &internal_asset, |l| l.can_manage_admins())?;
1481 let new_admin = *new_admin;
1482 let old_admin = PsmAdmin::<T>::try_mutate(
1483 &internal_asset,
1484 |maybe| -> Result<T::PalletsOrigin, DispatchError> {
1485 let admin = maybe.as_mut().ok_or(Error::<T>::PsmNotFound)?;
1486 let old = core::mem::replace(&mut admin.emergency_admin, new_admin.clone());
1487 Ok(old)
1488 },
1489 )?;
1490 Self::deposit_event(Event::EmergencyAdminChanged {
1491 internal_asset,
1492 old_admin: Box::new(old_admin),
1493 new_admin: Box::new(new_admin),
1494 });
1495 Ok(())
1496 }
1497 }
1498
1499 impl<T: Config> Pallet<T> {
1500 pub fn psm_account(internal_asset: &T::AssetId) -> T::AccountId {
1503 let entropy = (<PalletId as TypeId>::TYPE_ID, T::PalletId::get(), internal_asset)
1504 .using_encoded(sp_io::hashing::blake2_256);
1505 T::AccountId::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
1506 .expect("All byte sequences are valid `AccountId`s; qed")
1507 }
1508
1509 #[cfg(test)]
1512 pub(crate) fn max_psm_debt(internal_asset: &T::AssetId) -> BalanceOf<T> {
1513 Psm::<T>::get(internal_asset).map(|p| p.max_debt).unwrap_or_default()
1514 }
1515
1516 pub(crate) fn max_asset_debt(
1522 internal_asset: &T::AssetId,
1523 external_asset: &T::AssetId,
1524 info: &PsmInfo<T>,
1525 ) -> BalanceOf<T> {
1526 let asset_weight = AssetCeilingWeight::<T>::get(internal_asset, external_asset);
1527 let total_weight = Self::total_ceiling_weight(internal_asset);
1528 Self::normalised_ceiling(asset_weight, total_weight, info.max_debt)
1529 }
1530
1531 fn total_ceiling_weight(internal_asset: &T::AssetId) -> u32 {
1533 AssetCeilingWeight::<T>::iter_prefix(internal_asset)
1534 .map(|(_, w)| w.deconstruct())
1535 .fold(0u32, |acc, x| acc.saturating_add(x))
1536 }
1537
1538 fn normalised_ceiling(
1541 asset_weight: Permill,
1542 total_weight: u32,
1543 max_debt: BalanceOf<T>,
1544 ) -> BalanceOf<T> {
1545 let weight = asset_weight.deconstruct();
1546 if weight == 0 || total_weight == 0 {
1547 return BalanceOf::<T>::zero();
1548 }
1549 Perbill::from_rational(weight, total_weight).mul_floor(max_debt)
1550 }
1551
1552 pub(crate) fn total_psm_debt(internal_asset: &T::AssetId) -> BalanceOf<T> {
1554 PsmDebt::<T>::iter_prefix_values(internal_asset)
1555 .fold(BalanceOf::<T>::zero(), |acc, debt| acc.saturating_add(debt))
1556 }
1557
1558 #[cfg(test)]
1560 pub(crate) fn is_approved_asset(
1561 internal_asset: &T::AssetId,
1562 external_asset: &T::AssetId,
1563 ) -> bool {
1564 ExternalAssets::<T>::contains_key(internal_asset, external_asset)
1565 }
1566
1567 pub(crate) fn get_reserve(
1569 internal_asset: &T::AssetId,
1570 external_asset: &T::AssetId,
1571 ) -> BalanceOf<T> {
1572 T::Fungibles::balance(external_asset.clone(), &Self::psm_account(internal_asset))
1573 }
1574
1575 pub(crate) fn external_to_internal(
1581 amount: BalanceOf<T>,
1582 ext_decimals: u8,
1583 internal_decimals: u8,
1584 ) -> Result<BalanceOf<T>, Error<T>> {
1585 use core::cmp::Ordering::*;
1586 match ext_decimals.cmp(&internal_decimals) {
1587 Equal => Ok(amount),
1588 Less => {
1589 let diff = (internal_decimals - ext_decimals) as u32;
1590 let factor = Self::pow10(diff)?;
1591 amount.checked_mul(&factor).ok_or(Error::<T>::ConversionOverflow)
1592 },
1593 Greater => {
1594 let diff = (ext_decimals - internal_decimals) as u32;
1595 let factor = Self::pow10(diff)?;
1596 Ok(amount.checked_div(&factor).unwrap_or_else(BalanceOf::<T>::zero))
1597 },
1598 }
1599 }
1600
1601 pub(crate) fn internal_to_external(
1606 amount: BalanceOf<T>,
1607 ext_decimals: u8,
1608 internal_decimals: u8,
1609 ) -> Result<BalanceOf<T>, Error<T>> {
1610 use core::cmp::Ordering::*;
1611 match ext_decimals.cmp(&internal_decimals) {
1612 Equal => Ok(amount),
1613 Less => {
1614 let diff = (internal_decimals - ext_decimals) as u32;
1615 let factor = Self::pow10(diff)?;
1616 Ok(amount.checked_div(&factor).unwrap_or_else(BalanceOf::<T>::zero))
1617 },
1618 Greater => {
1619 let diff = (ext_decimals - internal_decimals) as u32;
1620 let factor = Self::pow10(diff)?;
1621 amount.checked_mul(&factor).ok_or(Error::<T>::ConversionOverflow)
1622 },
1623 }
1624 }
1625
1626 fn pow10(exp: u32) -> Result<BalanceOf<T>, Error<T>> {
1629 let factor_u128 = 10u128.checked_pow(exp).ok_or(Error::<T>::ConversionOverflow)?;
1630 factor_u128.try_into().map_err(|_| Error::<T>::ConversionOverflow)
1631 }
1632
1633 pub(crate) fn ensure_decimals_match(
1637 info: &PsmInfo<T>,
1638 internal_asset: &T::AssetId,
1639 external_asset: &T::AssetId,
1640 external: &ExternalAssetInfo,
1641 ) -> Result<(u8, u8), DispatchError> {
1642 ensure!(
1643 T::Fungibles::decimals(external_asset.clone()) == external.decimals,
1644 Error::<T>::DecimalsMismatch
1645 );
1646 ensure!(
1647 T::Fungibles::decimals(internal_asset.clone()) == info.internal_decimals,
1648 Error::<T>::DecimalsMismatch
1649 );
1650 Ok((external.decimals, info.internal_decimals))
1651 }
1652
1653 pub(crate) fn ensure_psm_admin(
1660 origin: OriginFor<T>,
1661 internal_asset: &T::AssetId,
1662 required: impl Fn(PsmManagerLevel) -> bool,
1663 ) -> DispatchResult {
1664 let admin = PsmAdmin::<T>::get(internal_asset).ok_or(Error::<T>::PsmNotFound)?;
1665 let caller = <T as Config>::RuntimeOrigin::from(origin).into_caller();
1666 let level = if caller == admin.full_admin {
1667 PsmManagerLevel::Full
1668 } else if caller == admin.emergency_admin {
1669 PsmManagerLevel::Emergency
1670 } else {
1671 return Err(DispatchError::BadOrigin);
1672 };
1673 ensure!(required(level), Error::<T>::InsufficientPrivilege);
1674 Ok(())
1675 }
1676
1677 #[cfg(any(feature = "try-runtime", test))]
1678 pub(crate) fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
1679 use sp_runtime::traits::CheckedAdd;
1680
1681 for (internal_asset, info) in Psm::<T>::iter() {
1682 ensure!(
1684 PsmAdmin::<T>::contains_key(&internal_asset),
1685 "PSM instance without a paired PsmAdmin record"
1686 );
1687
1688 let mut counted = 0u32;
1689 for (external_asset, external) in ExternalAssets::<T>::iter_prefix(&internal_asset)
1690 {
1691 counted = counted.saturating_add(1);
1692
1693 let debt = PsmDebt::<T>::get(&internal_asset, &external_asset);
1695 let reserve = Self::get_reserve(&internal_asset, &external_asset);
1696 let debt_as_external =
1697 Self::internal_to_external(debt, external.decimals, info.internal_decimals)
1698 .map_err(|_| "Failed to convert tracked debt to external units")?;
1699 ensure!(
1700 reserve >= debt_as_external,
1701 "PSM reserve is less than tracked debt for an asset"
1702 );
1703 }
1704
1705 ensure!(
1707 info.external_count == counted,
1708 "PsmInfo.external_count does not match the approved externals"
1709 );
1710
1711 let mut sum = BalanceOf::<T>::zero();
1713 for (_, debt) in PsmDebt::<T>::iter_prefix(&internal_asset) {
1714 sum = sum.checked_add(&debt).ok_or("PSM debt overflow when summing")?;
1715 }
1716 ensure!(
1717 sum == Self::total_psm_debt(&internal_asset),
1718 "sum of per-asset debts disagrees with total_psm_debt"
1719 );
1720 }
1721
1722 for (internal_asset, _, _) in ExternalAssets::<T>::iter() {
1724 ensure!(
1725 Psm::<T>::contains_key(&internal_asset),
1726 "Orphaned ExternalAssets row without parent PSM"
1727 );
1728 }
1729 for (internal_asset, _, _) in PsmDebt::<T>::iter() {
1730 ensure!(
1731 Psm::<T>::contains_key(&internal_asset),
1732 "Orphaned PsmDebt row without parent PSM"
1733 );
1734 }
1735 for (internal_asset, _) in PsmAdmin::<T>::iter() {
1736 ensure!(
1737 Psm::<T>::contains_key(&internal_asset),
1738 "Orphaned PsmAdmin row without parent PSM"
1739 );
1740 }
1741
1742 Ok(())
1743 }
1744 }
1745}