pallet_moonbeam_foreign_assets/
lib.rs

1// Copyright 2025 Moonbeam Foundation.
2// This file is part of Moonbeam.
3
4// Moonbeam is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Moonbeam is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Moonbeam.  If not, see <http://www.gnu.org/licenses/>.
16
17//! # Moonbeam Foreign Assets pallet
18//!
19//! This pallets allow to create and manage XCM derivative assets (aka. foreign assets).
20//!
21//! Each asset is implemented by an evm smart contract that is deployed by this pallet
22//! The evm smart contract for each asset is trusted by the runtime, and should
23//! be deployed only by the runtime itself.
24//!
25//! This pallet made several assumptions on theses evm smarts contracts:
26//! - Only this pallet should be able to mint and burn tokens
27//! - The following selectors should be exposed and callable only by this pallet account:
28//!   - burnFrom(address, uint256)
29//!   - mintInto(address, uint256)
30//!   - pause(address, uint256)
31//!   - unpause(address, uint256)
32//! - The smart contract should expose as weel the ERC20.transfer selector
33//!
34//! Each asset has a unique identifier that can never change.
35//! This identifier is named "AssetId", it's an integer (u128).
36//! This pallet maintain a two-way mapping between each AssetId the XCM Location of the asset.
37
38#![cfg_attr(not(feature = "std"), no_std)]
39
40#[cfg(any(test, feature = "runtime-benchmarks"))]
41pub mod benchmarks;
42#[cfg(feature = "runtime-benchmarks")]
43pub use benchmarks::*;
44#[cfg(test)]
45pub mod mock;
46#[cfg(test)]
47pub mod tests;
48pub mod weights;
49
50mod evm;
51
52pub use pallet::*;
53pub use weights::WeightInfo;
54
55use self::evm::EvmCaller;
56use ethereum_types::{H160, U256};
57use frame_support::pallet_prelude::*;
58use frame_support::traits::Contains;
59use frame_support::{pallet, Deserialize, Serialize};
60use frame_system::pallet_prelude::*;
61use sp_std::{vec, vec::Vec};
62use xcm::latest::{
63	Asset, AssetId as XcmAssetId, Error as XcmError, Fungibility, Location, Result as XcmResult,
64	XcmContext,
65};
66use xcm::prelude::Parachain;
67use xcm_executor::traits::ConvertLocation;
68use xcm_executor::traits::Error as MatchError;
69
70const FOREIGN_ASSETS_PREFIX: [u8; 4] = [0xff, 0xff, 0xff, 0xff];
71
72/// Trait for the OnForeignAssetRegistered hook
73pub trait ForeignAssetCreatedHook<ForeignAsset> {
74	fn on_asset_created(foreign_asset: &ForeignAsset, asset_id: &AssetId);
75}
76
77impl<ForeignAsset> ForeignAssetCreatedHook<ForeignAsset> for () {
78	fn on_asset_created(_foreign_asset: &ForeignAsset, _asset_id: &AssetId) {}
79}
80
81/// Ensure origin location is a sibling
82fn convert_location<T>(location: &Location) -> Result<T::AccountId, DispatchError>
83where
84	T: Config,
85{
86	match location.unpack() {
87		(1, [Parachain(_)]) => T::ConvertLocation::convert_location(location)
88			.ok_or(Error::<T>::CannotConvertLocationToAccount.into()),
89		_ => Err(DispatchError::BadOrigin.into()),
90	}
91}
92#[derive(Decode, Encode, Debug, PartialEq, TypeInfo, Clone)]
93pub enum OriginType {
94	XCM(Location),
95	Governance,
96}
97
98/// Used to convert the success of an EnsureOrigin into `OriginType::Governance`
99pub struct MapSuccessToGovernance<Original>(PhantomData<Original>);
100impl<O, Original: EnsureOrigin<O, Success = ()>> EnsureOrigin<O>
101	for MapSuccessToGovernance<Original>
102{
103	type Success = OriginType;
104	fn try_origin(o: O) -> Result<OriginType, O> {
105		Original::try_origin(o)?;
106		Ok(OriginType::Governance)
107	}
108	#[cfg(feature = "runtime-benchmarks")]
109	fn try_successful_origin() -> Result<O, ()> {
110		Original::try_successful_origin()
111	}
112}
113
114/// Used to convert the success of an EnsureOrigin into `OriginType::XCM`
115pub struct MapSuccessToXcm<Original>(PhantomData<Original>);
116impl<O, Original: EnsureOrigin<O, Success = Location>> EnsureOrigin<O>
117	for MapSuccessToXcm<Original>
118{
119	type Success = OriginType;
120	fn try_origin(o: O) -> Result<OriginType, O> {
121		Original::try_origin(o).map(OriginType::XCM)
122	}
123	#[cfg(feature = "runtime-benchmarks")]
124	fn try_successful_origin() -> Result<O, ()> {
125		Original::try_successful_origin()
126	}
127}
128
129pub(crate) struct ForeignAssetsMatcher<T>(core::marker::PhantomData<T>);
130
131impl<T: crate::Config> ForeignAssetsMatcher<T> {
132	fn match_asset(asset: &Asset) -> Result<(AssetId, H160, U256, AssetStatus), MatchError> {
133		let (amount, location) = match (&asset.fun, &asset.id) {
134			(Fungibility::Fungible(ref amount), XcmAssetId(ref location)) => (amount, location),
135			_ => return Err(MatchError::AssetNotHandled),
136		};
137
138		if let Some((asset_id, asset_status)) = AssetsByLocation::<T>::get(&location) {
139			Ok((
140				asset_id,
141				Pallet::<T>::contract_address_from_asset_id(asset_id),
142				U256::from(*amount),
143				asset_status,
144			))
145		} else {
146			Err(MatchError::AssetNotHandled)
147		}
148	}
149}
150
151#[derive(Decode, Debug, Encode, PartialEq, TypeInfo)]
152pub enum AssetStatus {
153	/// All operations are enabled
154	Active,
155	/// The asset is frozen, but deposit from XCM still work
156	FrozenXcmDepositAllowed,
157	/// The asset is frozen, and deposit from XCM will fail
158	FrozenXcmDepositForbidden,
159}
160
161impl AssetStatus {
162	pub fn is_frozen(&self) -> bool {
163		matches!(
164			self,
165			AssetStatus::FrozenXcmDepositAllowed | AssetStatus::FrozenXcmDepositForbidden
166		)
167	}
168}
169
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171pub struct EvmForeignAssetInfo {
172	pub asset_id: AssetId,
173	pub xcm_location: Location,
174	pub decimals: u8,
175	pub symbol: BoundedVec<u8, ConstU32<256>>,
176	pub name: BoundedVec<u8, ConstU32<256>>,
177}
178
179#[pallet]
180pub mod pallet {
181	use super::*;
182	use frame_support::traits::{Currency, ReservableCurrency};
183	use pallet_evm::{GasWeightMapping, Runner};
184	use sp_runtime::traits::{AccountIdConversion, AtLeast32BitUnsigned, Convert};
185	use xcm_executor::traits::ConvertLocation;
186	use xcm_executor::traits::Error as MatchError;
187	use xcm_executor::AssetsInHolding;
188
189	#[pallet::pallet]
190	#[pallet::without_storage_info]
191	pub struct Pallet<T>(PhantomData<T>);
192
193	/// The moonbeam foreign assets's pallet id
194	pub const PALLET_ID: frame_support::PalletId = frame_support::PalletId(*b"forgasst");
195
196	#[pallet::config]
197	pub trait Config:
198		frame_system::Config<RuntimeEvent: From<Event<Self>>>
199		+ pallet_evm::Config
200		+ scale_info::TypeInfo
201	{
202		// Convert AccountId to H160
203		type AccountIdToH160: Convert<Self::AccountId, H160>;
204
205		/// A filter to forbid some AssetId values, if you don't use it, put "Everything"
206		type AssetIdFilter: Contains<AssetId>;
207
208		/// EVM runner
209		type EvmRunner: Runner<Self>;
210
211		type ConvertLocation: ConvertLocation<Self::AccountId>;
212
213		/// Origin that is allowed to create a new foreign assets
214		type ForeignAssetCreatorOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = OriginType>;
215
216		/// Origin that is allowed to freeze all tokens of a foreign asset
217		type ForeignAssetFreezerOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = OriginType>;
218
219		/// Origin that is allowed to modify asset information for foreign assets
220		type ForeignAssetModifierOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = OriginType>;
221
222		/// Origin that is allowed to unfreeze all tokens of a foreign asset that was previously
223		/// frozen
224		type ForeignAssetUnfreezerOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = OriginType>;
225
226		/// Hook to be called when new foreign asset is registered.
227		type OnForeignAssetCreated: ForeignAssetCreatedHook<Location>;
228
229		/// Maximum numbers of different foreign assets
230		type MaxForeignAssets: Get<u32>;
231
232		/// Weight information for extrinsics in this pallet.
233		type WeightInfo: WeightInfo;
234
235		// Convert XCM Location to H160
236		type XcmLocationToH160: ConvertLocation<H160>;
237
238		/// Amount of tokens required to lock for creating a new foreign asset
239		type ForeignAssetCreationDeposit: Get<BalanceOf<Self>>;
240
241		/// The balance type for locking funds
242		type Balance: Member
243			+ Parameter
244			+ AtLeast32BitUnsigned
245			+ Default
246			+ Copy
247			+ MaybeSerializeDeserialize
248			+ MaxEncodedLen
249			+ TypeInfo;
250
251		/// The currency type for locking funds
252		type Currency: ReservableCurrency<Self::AccountId>;
253	}
254
255	type BalanceOf<T> =
256		<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
257
258	pub type AssetBalance = U256;
259	pub type AssetId = u128;
260
261	/// An error that can occur while executing the mapping pallet's logic.
262	#[pallet::error]
263	pub enum Error<T> {
264		AssetAlreadyExists,
265		AssetAlreadyFrozen,
266		AssetDoesNotExist,
267		AssetIdFiltered,
268		AssetNotFrozen,
269		CorruptedStorageOrphanLocation,
270		Erc20ContractCreationFail,
271		EvmCallPauseFail,
272		EvmCallUnpauseFail,
273		EvmCallMintIntoFail,
274		EvmCallTransferFail,
275		EvmInternalError,
276		/// Account has insufficient balance for locking
277		InsufficientBalance,
278		CannotConvertLocationToAccount,
279		LocationOutsideOfOrigin,
280		AssetNotInSiblingPara,
281		InvalidSymbol,
282		InvalidTokenName,
283		LocationAlreadyExists,
284		NoPendingDeposit,
285		AssetNotActive,
286		TooManyForeignAssets,
287	}
288
289	#[pallet::event]
290	#[pallet::generate_deposit(pub(crate) fn deposit_event)]
291	pub enum Event<T: Config> {
292		/// New asset with the asset manager is registered
293		ForeignAssetCreated {
294			contract_address: H160,
295			asset_id: AssetId,
296			xcm_location: Location,
297			deposit: Option<BalanceOf<T>>,
298		},
299		/// Changed the xcm type mapping for a given asset id
300		ForeignAssetXcmLocationChanged {
301			asset_id: AssetId,
302			previous_xcm_location: Location,
303			new_xcm_location: Location,
304		},
305		// Freezes all tokens of a given asset id
306		ForeignAssetFrozen {
307			asset_id: AssetId,
308			xcm_location: Location,
309		},
310		// Thawing a previously frozen asset
311		ForeignAssetUnfrozen {
312			asset_id: AssetId,
313			xcm_location: Location,
314		},
315		/// Tokens have been locked for asset creation
316		TokensLocked(T::AccountId, AssetId, AssetBalance),
317		/// A deposit was recorded as pending because the asset is frozen
318		PendingDepositRecorded {
319			asset_id: AssetId,
320			beneficiary: H160,
321			amount: U256,
322			total_pending: U256,
323		},
324		/// A pending deposit was claimed and minted
325		PendingDepositClaimed {
326			asset_id: AssetId,
327			beneficiary: H160,
328			amount: U256,
329		},
330	}
331
332	/// Mapping from an asset id to a Foreign asset type.
333	/// This is mostly used when receiving transaction specifying an asset directly,
334	/// like transferring an asset from this chain to another.
335	#[pallet::storage]
336	#[pallet::getter(fn assets_by_id)]
337	pub type AssetsById<T: Config> =
338		CountedStorageMap<_, Blake2_128Concat, AssetId, Location, OptionQuery>;
339
340	/// Reverse mapping of AssetsById. Mapping from a foreign asset to an asset id.
341	/// This is mostly used when receiving a multilocation XCM message to retrieve
342	/// the corresponding asset in which tokens should me minted.
343	#[pallet::storage]
344	#[pallet::getter(fn assets_by_location)]
345	pub type AssetsByLocation<T: Config> =
346		StorageMap<_, Blake2_128Concat, Location, (AssetId, AssetStatus)>;
347
348	/// Mapping from an asset id to its creation details
349	#[pallet::storage]
350	#[pallet::getter(fn assets_creation_details)]
351	pub type AssetsCreationDetails<T: Config> =
352		StorageMap<_, Blake2_128Concat, AssetId, AssetDepositDetails<T>>;
353
354	/// Pending deposits for frozen assets, keyed by (asset_id, beneficiary).
355	/// Deposits for the same (asset_id, beneficiary) accumulate via checked_add.
356	#[pallet::storage]
357	#[pallet::getter(fn pending_deposits)]
358	pub type PendingDeposits<T: Config> =
359		StorageDoubleMap<_, Blake2_128Concat, AssetId, Blake2_128Concat, H160, U256, OptionQuery>;
360
361	#[derive(Clone, Decode, Encode, Eq, PartialEq, Debug, TypeInfo, MaxEncodedLen)]
362	pub struct AssetDepositDetails<T: Config> {
363		pub deposit_account: T::AccountId,
364		pub deposit: BalanceOf<T>,
365	}
366
367	#[pallet::genesis_config]
368	pub struct GenesisConfig<T: Config> {
369		pub assets: Vec<EvmForeignAssetInfo>,
370		pub _phantom: PhantomData<T>,
371	}
372
373	impl<T: Config> Default for GenesisConfig<T> {
374		fn default() -> Self {
375			Self {
376				assets: vec![],
377				_phantom: Default::default(),
378			}
379		}
380	}
381
382	#[pallet::genesis_build]
383	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
384		fn build(&self) {
385			for asset in self.assets.clone() {
386				Pallet::<T>::register_foreign_asset(
387					asset.asset_id,
388					asset.xcm_location,
389					asset.decimals,
390					asset.symbol,
391					asset.name,
392				)
393				.expect("couldn't register asset");
394			}
395		}
396	}
397
398	impl<T: Config> Pallet<T> {
399		/// The account ID of this pallet
400		#[inline]
401		pub fn account_id() -> H160 {
402			let account_id: T::AccountId = PALLET_ID.into_account_truncating();
403			T::AccountIdToH160::convert(account_id)
404		}
405
406		/// Compute asset contract address from asset id
407		#[inline]
408		pub fn contract_address_from_asset_id(asset_id: AssetId) -> H160 {
409			let mut buffer = [0u8; 20];
410			buffer[..4].copy_from_slice(&FOREIGN_ASSETS_PREFIX);
411			buffer[4..].copy_from_slice(&asset_id.to_be_bytes());
412			H160(buffer)
413		}
414
415		/// This method only exists for migration purposes and will be deleted once the
416		/// foreign assets migration is finished.
417		pub fn register_foreign_asset(
418			asset_id: AssetId,
419			xcm_location: Location,
420			decimals: u8,
421			symbol: BoundedVec<u8, ConstU32<256>>,
422			name: BoundedVec<u8, ConstU32<256>>,
423		) -> DispatchResult {
424			Self::do_create_asset(asset_id, xcm_location, decimals, symbol, name, None)
425		}
426
427		/// Mint an asset into a specific account
428		pub fn mint_into(
429			asset_id: AssetId,
430			beneficiary: T::AccountId,
431			amount: U256,
432		) -> Result<(), evm::EvmError> {
433			// We perform the evm call in a storage transaction to ensure that if it fail
434			// any contract storage changes are rolled back.
435			frame_support::storage::with_storage_layer(|| {
436				EvmCaller::<T>::erc20_mint_into(
437					Self::contract_address_from_asset_id(asset_id),
438					T::AccountIdToH160::convert(beneficiary),
439					amount,
440				)
441			})
442			.map_err(Into::into)
443		}
444
445		/// Transfer an asset from an account to another one
446		pub fn transfer(
447			asset_id: AssetId,
448			from: T::AccountId,
449			to: T::AccountId,
450			amount: U256,
451		) -> Result<(), evm::EvmError> {
452			frame_support::storage::with_storage_layer(|| {
453				EvmCaller::<T>::erc20_transfer(
454					Self::contract_address_from_asset_id(asset_id),
455					T::AccountIdToH160::convert(from),
456					T::AccountIdToH160::convert(to),
457					amount,
458				)
459			})
460			.map_err(Into::into)
461		}
462
463		pub fn balance(asset_id: AssetId, who: T::AccountId) -> Result<U256, evm::EvmError> {
464			EvmCaller::<T>::erc20_balance_of(asset_id, T::AccountIdToH160::convert(who))
465				.map_err(Into::into)
466		}
467
468		/// Approve a spender to spend a certain amount of tokens from the owner account
469		pub fn approve(
470			asset_id: AssetId,
471			owner: T::AccountId,
472			spender: T::AccountId,
473			amount: U256,
474		) -> Result<(), evm::EvmError> {
475			// We perform the evm call in a storage transaction to ensure that if it fail
476			// any contract storage changes are rolled back.
477			frame_support::storage::with_storage_layer(|| {
478				EvmCaller::<T>::erc20_approve(
479					Self::contract_address_from_asset_id(asset_id),
480					T::AccountIdToH160::convert(owner),
481					T::AccountIdToH160::convert(spender),
482					amount,
483				)
484			})
485			.map_err(Into::into)
486		}
487
488		pub fn weight_of_erc20_burn() -> Weight {
489			T::GasWeightMapping::gas_to_weight(evm::ERC20_BURN_FROM_GAS_LIMIT, true)
490		}
491		pub fn weight_of_erc20_mint() -> Weight {
492			T::GasWeightMapping::gas_to_weight(evm::ERC20_MINT_INTO_GAS_LIMIT, true)
493		}
494		pub fn weight_of_erc20_transfer() -> Weight {
495			T::GasWeightMapping::gas_to_weight(evm::ERC20_TRANSFER_GAS_LIMIT, true)
496		}
497		#[cfg(feature = "runtime-benchmarks")]
498		pub fn set_asset(asset_location: Location, asset_id: AssetId) {
499			AssetsByLocation::<T>::insert(&asset_location, (asset_id, AssetStatus::Active));
500			AssetsById::<T>::insert(&asset_id, asset_location);
501		}
502
503		#[cfg(feature = "runtime-benchmarks")]
504		pub fn create_asset_contract(
505			asset_id: AssetId,
506			decimals: u8,
507			symbol: &str,
508			name: &str,
509		) -> Result<H160, Error<T>> {
510			EvmCaller::<T>::erc20_create(asset_id, decimals, symbol, name)
511		}
512	}
513
514	#[pallet::call]
515	impl<T: Config> Pallet<T> {
516		/// Create new asset with the ForeignAssetCreator
517		#[pallet::call_index(0)]
518		#[pallet::weight(<T as Config>::WeightInfo::create_foreign_asset())]
519		pub fn create_foreign_asset(
520			origin: OriginFor<T>,
521			asset_id: AssetId,
522			asset_xcm_location: Location,
523			decimals: u8,
524			symbol: BoundedVec<u8, ConstU32<256>>,
525			name: BoundedVec<u8, ConstU32<256>>,
526		) -> DispatchResult {
527			let origin_type = T::ForeignAssetCreatorOrigin::ensure_origin(origin.clone())?;
528
529			Self::ensure_origin_can_modify_location(origin_type.clone(), &asset_xcm_location)?;
530			let deposit_account = Self::get_deposit_account(origin_type)?;
531
532			Self::do_create_asset(
533				asset_id,
534				asset_xcm_location,
535				decimals,
536				symbol,
537				name,
538				deposit_account,
539			)
540		}
541
542		/// Change the xcm type mapping for a given assetId
543		/// We also change this if the previous units per second where pointing at the old
544		/// assetType
545		#[pallet::call_index(1)]
546		#[pallet::weight(<T as Config>::WeightInfo::change_xcm_location())]
547		pub fn change_xcm_location(
548			origin: OriginFor<T>,
549			asset_id: AssetId,
550			new_xcm_location: Location,
551		) -> DispatchResult {
552			let origin_type = T::ForeignAssetModifierOrigin::ensure_origin(origin.clone())?;
553
554			Self::ensure_origin_can_modify_location(origin_type.clone(), &new_xcm_location)?;
555
556			let previous_location =
557				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
558
559			Self::ensure_origin_can_modify_location(origin_type, &previous_location)?;
560
561			Self::do_change_xcm_location(asset_id, previous_location, new_xcm_location)
562		}
563
564		/// Freeze a given foreign assetId
565		#[pallet::call_index(2)]
566		#[pallet::weight(<T as Config>::WeightInfo::freeze_foreign_asset())]
567		pub fn freeze_foreign_asset(
568			origin: OriginFor<T>,
569			asset_id: AssetId,
570			allow_xcm_deposit: bool,
571		) -> DispatchResult {
572			let origin_type = T::ForeignAssetFreezerOrigin::ensure_origin(origin.clone())?;
573
574			let xcm_location =
575				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
576
577			Self::ensure_origin_can_modify_location(origin_type, &xcm_location)?;
578
579			Self::do_freeze_asset(asset_id, xcm_location, allow_xcm_deposit)
580		}
581
582		/// Unfreeze a given foreign assetId
583		#[pallet::call_index(3)]
584		#[pallet::weight(<T as Config>::WeightInfo::unfreeze_foreign_asset())]
585		pub fn unfreeze_foreign_asset(origin: OriginFor<T>, asset_id: AssetId) -> DispatchResult {
586			let origin_type = T::ForeignAssetUnfreezerOrigin::ensure_origin(origin.clone())?;
587
588			let xcm_location =
589				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
590
591			Self::ensure_origin_can_modify_location(origin_type, &xcm_location)?;
592
593			Self::do_unfreeze_asset(asset_id, xcm_location)
594		}
595
596		/// Claim a pending deposit for a given asset and beneficiary.
597		/// Callable by any signed origin (permissionless). Tokens are minted to the
598		/// beneficiary, not the caller. Requires the asset to be active (unfrozen).
599		#[pallet::call_index(4)]
600		#[pallet::weight(<T as Config>::WeightInfo::claim_pending_deposit())]
601		pub fn claim_pending_deposit(
602			origin: OriginFor<T>,
603			asset_id: AssetId,
604			beneficiary: H160,
605		) -> DispatchResult {
606			ensure_signed(origin)?;
607
608			let xcm_location =
609				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
610			let (_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
611				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
612
613			ensure!(
614				asset_status == AssetStatus::Active,
615				Error::<T>::AssetNotActive
616			);
617
618			let amount = PendingDeposits::<T>::get(asset_id, beneficiary)
619				.ok_or(Error::<T>::NoPendingDeposit)?;
620
621			let contract_address = Self::contract_address_from_asset_id(asset_id);
622
623			// Both the storage removal and the EVM mint run inside the same
624			// transactional layer so they are rolled back atomically on failure.
625			frame_support::storage::with_storage_layer(|| {
626				PendingDeposits::<T>::remove(asset_id, beneficiary);
627				EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount)
628			})
629			.map_err(|_| Error::<T>::EvmCallMintIntoFail)?;
630
631			Self::deposit_event(Event::PendingDepositClaimed {
632				asset_id,
633				beneficiary,
634				amount,
635			});
636
637			Ok(())
638		}
639	}
640
641	impl<T: Config> Pallet<T> {
642		/// Ensure that the caller origin can modify the location,
643		fn ensure_origin_can_modify_location(
644			origin_type: OriginType,
645			location: &Location,
646		) -> DispatchResult {
647			match origin_type {
648				OriginType::XCM(origin_location) => {
649					ensure!(
650						location.starts_with(&origin_location),
651						Error::<T>::LocationOutsideOfOrigin,
652					);
653				}
654				OriginType::Governance => {
655					// nothing to check Governance can change any asset
656				}
657			};
658			Ok(())
659		}
660
661		fn get_deposit_account(
662			origin_type: OriginType,
663		) -> Result<Option<T::AccountId>, DispatchError> {
664			match origin_type {
665				OriginType::XCM(origin_location) => {
666					let deposit_account = convert_location::<T>(&origin_location)?;
667					Ok(Some(deposit_account))
668				}
669				OriginType::Governance => Ok(None),
670			}
671		}
672
673		pub fn do_create_asset(
674			asset_id: AssetId,
675			asset_xcm_location: Location,
676			decimals: u8,
677			symbol: BoundedVec<u8, ConstU32<256>>,
678			name: BoundedVec<u8, ConstU32<256>>,
679			deposit_account: Option<T::AccountId>,
680		) -> DispatchResult {
681			ensure!(
682				!AssetsById::<T>::contains_key(&asset_id),
683				Error::<T>::AssetAlreadyExists
684			);
685
686			ensure!(
687				!AssetsByLocation::<T>::contains_key(&asset_xcm_location),
688				Error::<T>::LocationAlreadyExists
689			);
690
691			ensure!(
692				AssetsById::<T>::count() < T::MaxForeignAssets::get(),
693				Error::<T>::TooManyForeignAssets
694			);
695
696			ensure!(
697				T::AssetIdFilter::contains(&asset_id),
698				Error::<T>::AssetIdFiltered
699			);
700
701			let symbol = core::str::from_utf8(&symbol).map_err(|_| Error::<T>::InvalidSymbol)?;
702			let name = core::str::from_utf8(&name).map_err(|_| Error::<T>::InvalidTokenName)?;
703			let contract_address = EvmCaller::<T>::erc20_create(asset_id, decimals, symbol, name)?;
704
705			let deposit = if let Some(deposit_account) = deposit_account {
706				let deposit = T::ForeignAssetCreationDeposit::get();
707
708				// Reserve _deposit_ amount of funds from the caller
709				<T as Config>::Currency::reserve(&deposit_account, deposit)?;
710
711				// Insert the amount that is reserved from the user
712				AssetsCreationDetails::<T>::insert(
713					&asset_id,
714					AssetDepositDetails {
715						deposit_account,
716						deposit,
717					},
718				);
719
720				Some(deposit)
721			} else {
722				None
723			};
724
725			// Insert the association assetId->foreigAsset
726			// Insert the association foreigAsset->assetId
727			AssetsById::<T>::insert(&asset_id, &asset_xcm_location);
728			AssetsByLocation::<T>::insert(&asset_xcm_location, (asset_id, AssetStatus::Active));
729
730			T::OnForeignAssetCreated::on_asset_created(&asset_xcm_location, &asset_id);
731
732			Self::deposit_event(Event::ForeignAssetCreated {
733				contract_address,
734				asset_id,
735				xcm_location: asset_xcm_location,
736				deposit,
737			});
738			Ok(())
739		}
740
741		pub fn do_change_xcm_location(
742			asset_id: AssetId,
743			previous_xcm_location: Location,
744			new_xcm_location: Location,
745		) -> DispatchResult {
746			ensure!(
747				!AssetsByLocation::<T>::contains_key(&new_xcm_location),
748				Error::<T>::LocationAlreadyExists
749			);
750
751			// Remove previous foreign asset info
752			let (_asset_id, asset_status) = AssetsByLocation::<T>::take(&previous_xcm_location)
753				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
754
755			// Insert new foreign asset info
756			AssetsById::<T>::insert(&asset_id, &new_xcm_location);
757			AssetsByLocation::<T>::insert(&new_xcm_location, (asset_id, asset_status));
758
759			Self::deposit_event(Event::ForeignAssetXcmLocationChanged {
760				asset_id,
761				new_xcm_location,
762				previous_xcm_location,
763			});
764			Ok(())
765		}
766
767		pub fn do_freeze_asset(
768			asset_id: AssetId,
769			xcm_location: Location,
770			allow_xcm_deposit: bool,
771		) -> DispatchResult {
772			let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
773				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
774
775			ensure!(!asset_status.is_frozen(), Error::<T>::AssetAlreadyFrozen);
776
777			EvmCaller::<T>::erc20_pause(asset_id)?;
778
779			let new_asset_status = if allow_xcm_deposit {
780				AssetStatus::FrozenXcmDepositAllowed
781			} else {
782				AssetStatus::FrozenXcmDepositForbidden
783			};
784
785			AssetsByLocation::<T>::insert(&xcm_location, (asset_id, new_asset_status));
786
787			Self::deposit_event(Event::ForeignAssetFrozen {
788				asset_id,
789				xcm_location,
790			});
791			Ok(())
792		}
793
794		pub fn do_unfreeze_asset(asset_id: AssetId, xcm_location: Location) -> DispatchResult {
795			let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
796				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
797
798			ensure!(asset_status.is_frozen(), Error::<T>::AssetNotFrozen);
799
800			EvmCaller::<T>::erc20_unpause(asset_id)?;
801
802			AssetsByLocation::<T>::insert(&xcm_location, (asset_id, AssetStatus::Active));
803
804			Self::deposit_event(Event::ForeignAssetUnfrozen {
805				asset_id,
806				xcm_location,
807			});
808			Ok(())
809		}
810	}
811
812	impl<T: Config> xcm_executor::traits::TransactAsset for Pallet<T> {
813		// For optimization reasons, the asset we want to deposit has not really been withdrawn,
814		// we have just traced from which account it should have been withdrawn.
815		// So we will retrieve these information and make the transfer from the origin account.
816		fn deposit_asset(what: &Asset, who: &Location, _context: Option<&XcmContext>) -> XcmResult {
817			let (asset_id, contract_address, amount, asset_status) =
818				ForeignAssetsMatcher::<T>::match_asset(what)?;
819
820			if let AssetStatus::FrozenXcmDepositForbidden = asset_status {
821				return Err(XcmError::FailedToTransactAsset(
822					"asset is frozen and XCM deposits are forbidden",
823				));
824			}
825
826			let beneficiary = T::XcmLocationToH160::convert_location(who)
827				.ok_or(MatchError::AccountIdConversionFailed)?;
828
829			if matches!(asset_status, AssetStatus::FrozenXcmDepositAllowed) {
830				let total_pending = PendingDeposits::<T>::get(asset_id, beneficiary)
831					.unwrap_or(U256::zero())
832					.checked_add(amount)
833					.ok_or(XcmError::Overflow)?;
834
835				PendingDeposits::<T>::insert(asset_id, beneficiary, total_pending);
836
837				Pallet::<T>::deposit_event(Event::PendingDepositRecorded {
838					asset_id,
839					beneficiary,
840					amount,
841					total_pending,
842				});
843			} else {
844				// We perform the evm transfers in a storage transaction to ensure
845				// that if it fails any contract storage changes are rolled back.
846				frame_support::storage::with_storage_layer(|| {
847					EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount)
848				})?;
849			}
850
851			Ok(())
852		}
853
854		fn internal_transfer_asset(
855			asset: &Asset,
856			from: &Location,
857			to: &Location,
858			_context: &XcmContext,
859		) -> Result<AssetsInHolding, XcmError> {
860			let (_asset_id, contract_address, amount, asset_status) =
861				ForeignAssetsMatcher::<T>::match_asset(asset)?;
862
863			if asset_status.is_frozen() {
864				return Err(XcmError::FailedToTransactAsset("asset is frozen"));
865			}
866
867			let from = T::XcmLocationToH160::convert_location(from)
868				.ok_or(MatchError::AccountIdConversionFailed)?;
869
870			let to = T::XcmLocationToH160::convert_location(to)
871				.ok_or(MatchError::AccountIdConversionFailed)?;
872
873			// We perform the evm transfers in a storage transaction to ensure that if it fail
874			// any contract storage changes are rolled back.
875			frame_support::storage::with_storage_layer(|| {
876				EvmCaller::<T>::erc20_transfer(contract_address, from, to, amount)
877			})?;
878
879			Ok(asset.clone().into())
880		}
881
882		// Since we don't control the erc20 contract that manages the asset we want to withdraw,
883		// we can't really withdraw this asset, we can only transfer it to another account.
884		// It would be possible to transfer the asset to a dedicated account that would reflect
885		// the content of the xcm holding, but this would imply to perform two evm calls instead of
886		// one (1 to withdraw the asset and a second one to deposit it).
887		// In order to perform only one evm call, we just trace the origin of the asset,
888		// and then the transfer will only really be performed in the deposit instruction.
889		fn withdraw_asset(
890			what: &Asset,
891			who: &Location,
892			_context: Option<&XcmContext>,
893		) -> Result<AssetsInHolding, XcmError> {
894			let (_asset_id, contract_address, amount, asset_status) =
895				ForeignAssetsMatcher::<T>::match_asset(what)?;
896			let who = T::XcmLocationToH160::convert_location(who)
897				.ok_or(MatchError::AccountIdConversionFailed)?;
898
899			if asset_status.is_frozen() {
900				return Err(XcmError::FailedToTransactAsset("asset is frozen"));
901			}
902
903			// We perform the evm transfers in a storage transaction to ensure that if it fail
904			// any contract storage changes are rolled back.
905			frame_support::storage::with_storage_layer(|| {
906				EvmCaller::<T>::erc20_burn_from(contract_address, who, amount)
907			})?;
908
909			Ok(what.clone().into())
910		}
911
912		#[cfg(feature = "runtime-benchmarks")]
913		fn can_check_out(_dest: &Location, _what: &Asset, _context: &XcmContext) -> XcmResult {
914			// Needed for the benchmarks to work
915			Ok(())
916		}
917
918		#[cfg(feature = "runtime-benchmarks")]
919		fn check_out(_dest: &Location, _what: &Asset, _context: &XcmContext) {
920			// Needed for benchmarks to work
921		}
922	}
923
924	impl<T: Config> sp_runtime::traits::MaybeEquivalence<Location, AssetId> for Pallet<T> {
925		fn convert(location: &Location) -> Option<AssetId> {
926			AssetsByLocation::<T>::get(location).map(|(asset_id, _)| asset_id)
927		}
928		fn convert_back(asset_id: &AssetId) -> Option<Location> {
929			AssetsById::<T>::get(asset_id)
930		}
931	}
932}