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<(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				Pallet::<T>::contract_address_from_asset_id(asset_id),
141				U256::from(*amount),
142				asset_status,
143			))
144		} else {
145			Err(MatchError::AssetNotHandled)
146		}
147	}
148}
149
150#[derive(Decode, Debug, Encode, PartialEq, TypeInfo)]
151pub enum AssetStatus {
152	/// All operations are enabled
153	Active,
154	/// The asset is frozen, but deposit from XCM still work
155	FrozenXcmDepositAllowed,
156	/// The asset is frozen, and deposit from XCM will fail
157	FrozenXcmDepositForbidden,
158}
159
160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
161pub struct EvmForeignAssetInfo {
162	pub asset_id: AssetId,
163	pub xcm_location: Location,
164	pub decimals: u8,
165	pub symbol: BoundedVec<u8, ConstU32<256>>,
166	pub name: BoundedVec<u8, ConstU32<256>>,
167}
168
169#[pallet]
170pub mod pallet {
171	use super::*;
172	use frame_support::traits::{Currency, ReservableCurrency};
173	use pallet_evm::{GasWeightMapping, Runner};
174	use sp_runtime::traits::{AccountIdConversion, AtLeast32BitUnsigned, Convert};
175	use xcm_executor::traits::ConvertLocation;
176	use xcm_executor::traits::Error as MatchError;
177	use xcm_executor::AssetsInHolding;
178
179	#[pallet::pallet]
180	#[pallet::without_storage_info]
181	pub struct Pallet<T>(PhantomData<T>);
182
183	/// The moonbeam foreign assets's pallet id
184	pub const PALLET_ID: frame_support::PalletId = frame_support::PalletId(*b"forgasst");
185
186	#[pallet::config]
187	pub trait Config:
188		frame_system::Config<RuntimeEvent: From<Event<Self>>>
189		+ pallet_evm::Config
190		+ scale_info::TypeInfo
191	{
192		// Convert AccountId to H160
193		type AccountIdToH160: Convert<Self::AccountId, H160>;
194
195		/// A filter to forbid some AssetId values, if you don't use it, put "Everything"
196		type AssetIdFilter: Contains<AssetId>;
197
198		/// EVM runner
199		type EvmRunner: Runner<Self>;
200
201		type ConvertLocation: ConvertLocation<Self::AccountId>;
202
203		/// Origin that is allowed to create a new foreign assets
204		type ForeignAssetCreatorOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = OriginType>;
205
206		/// Origin that is allowed to freeze all tokens of a foreign asset
207		type ForeignAssetFreezerOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = OriginType>;
208
209		/// Origin that is allowed to modify asset information for foreign assets
210		type ForeignAssetModifierOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = OriginType>;
211
212		/// Origin that is allowed to unfreeze all tokens of a foreign asset that was previously
213		/// frozen
214		type ForeignAssetUnfreezerOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = OriginType>;
215
216		/// Hook to be called when new foreign asset is registered.
217		type OnForeignAssetCreated: ForeignAssetCreatedHook<Location>;
218
219		/// Maximum numbers of different foreign assets
220		type MaxForeignAssets: Get<u32>;
221
222		/// Weight information for extrinsics in this pallet.
223		type WeightInfo: WeightInfo;
224
225		// Convert XCM Location to H160
226		type XcmLocationToH160: ConvertLocation<H160>;
227
228		/// Amount of tokens required to lock for creating a new foreign asset
229		type ForeignAssetCreationDeposit: Get<BalanceOf<Self>>;
230
231		/// The balance type for locking funds
232		type Balance: Member
233			+ Parameter
234			+ AtLeast32BitUnsigned
235			+ Default
236			+ Copy
237			+ MaybeSerializeDeserialize
238			+ MaxEncodedLen
239			+ TypeInfo;
240
241		/// The currency type for locking funds
242		type Currency: ReservableCurrency<Self::AccountId>;
243	}
244
245	type BalanceOf<T> =
246		<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
247
248	pub type AssetBalance = U256;
249	pub type AssetId = u128;
250
251	/// An error that can occur while executing the mapping pallet's logic.
252	#[pallet::error]
253	pub enum Error<T> {
254		AssetAlreadyExists,
255		AssetAlreadyFrozen,
256		AssetDoesNotExist,
257		AssetIdFiltered,
258		AssetNotFrozen,
259		CorruptedStorageOrphanLocation,
260		Erc20ContractCreationFail,
261		EvmCallPauseFail,
262		EvmCallUnpauseFail,
263		EvmCallMintIntoFail,
264		EvmCallTransferFail,
265		EvmInternalError,
266		/// Account has insufficient balance for locking
267		InsufficientBalance,
268		CannotConvertLocationToAccount,
269		LocationOutsideOfOrigin,
270		AssetNotInSiblingPara,
271		InvalidSymbol,
272		InvalidTokenName,
273		LocationAlreadyExists,
274		TooManyForeignAssets,
275	}
276
277	#[pallet::event]
278	#[pallet::generate_deposit(pub(crate) fn deposit_event)]
279	pub enum Event<T: Config> {
280		/// New asset with the asset manager is registered
281		ForeignAssetCreated {
282			contract_address: H160,
283			asset_id: AssetId,
284			xcm_location: Location,
285			deposit: Option<BalanceOf<T>>,
286		},
287		/// Changed the xcm type mapping for a given asset id
288		ForeignAssetXcmLocationChanged {
289			asset_id: AssetId,
290			previous_xcm_location: Location,
291			new_xcm_location: Location,
292		},
293		// Freezes all tokens of a given asset id
294		ForeignAssetFrozen {
295			asset_id: AssetId,
296			xcm_location: Location,
297		},
298		// Thawing a previously frozen asset
299		ForeignAssetUnfrozen {
300			asset_id: AssetId,
301			xcm_location: Location,
302		},
303		/// Tokens have been locked for asset creation
304		TokensLocked(T::AccountId, AssetId, AssetBalance),
305	}
306
307	/// Mapping from an asset id to a Foreign asset type.
308	/// This is mostly used when receiving transaction specifying an asset directly,
309	/// like transferring an asset from this chain to another.
310	#[pallet::storage]
311	#[pallet::getter(fn assets_by_id)]
312	pub type AssetsById<T: Config> =
313		CountedStorageMap<_, Blake2_128Concat, AssetId, Location, OptionQuery>;
314
315	/// Reverse mapping of AssetsById. Mapping from a foreign asset to an asset id.
316	/// This is mostly used when receiving a multilocation XCM message to retrieve
317	/// the corresponding asset in which tokens should me minted.
318	#[pallet::storage]
319	#[pallet::getter(fn assets_by_location)]
320	pub type AssetsByLocation<T: Config> =
321		StorageMap<_, Blake2_128Concat, Location, (AssetId, AssetStatus)>;
322
323	/// Mapping from an asset id to its creation details
324	#[pallet::storage]
325	#[pallet::getter(fn assets_creation_details)]
326	pub type AssetsCreationDetails<T: Config> =
327		StorageMap<_, Blake2_128Concat, AssetId, AssetDepositDetails<T>>;
328
329	#[derive(Clone, Decode, Encode, Eq, PartialEq, Debug, TypeInfo, MaxEncodedLen)]
330	pub struct AssetDepositDetails<T: Config> {
331		pub deposit_account: T::AccountId,
332		pub deposit: BalanceOf<T>,
333	}
334
335	#[pallet::genesis_config]
336	pub struct GenesisConfig<T: Config> {
337		pub assets: Vec<EvmForeignAssetInfo>,
338		pub _phantom: PhantomData<T>,
339	}
340
341	impl<T: Config> Default for GenesisConfig<T> {
342		fn default() -> Self {
343			Self {
344				assets: vec![],
345				_phantom: Default::default(),
346			}
347		}
348	}
349
350	#[pallet::genesis_build]
351	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
352		fn build(&self) {
353			for asset in self.assets.clone() {
354				Pallet::<T>::register_foreign_asset(
355					asset.asset_id,
356					asset.xcm_location,
357					asset.decimals,
358					asset.symbol,
359					asset.name,
360				)
361				.expect("couldn't register asset");
362			}
363		}
364	}
365
366	impl<T: Config> Pallet<T> {
367		/// The account ID of this pallet
368		#[inline]
369		pub fn account_id() -> H160 {
370			let account_id: T::AccountId = PALLET_ID.into_account_truncating();
371			T::AccountIdToH160::convert(account_id)
372		}
373
374		/// Compute asset contract address from asset id
375		#[inline]
376		pub fn contract_address_from_asset_id(asset_id: AssetId) -> H160 {
377			let mut buffer = [0u8; 20];
378			buffer[..4].copy_from_slice(&FOREIGN_ASSETS_PREFIX);
379			buffer[4..].copy_from_slice(&asset_id.to_be_bytes());
380			H160(buffer)
381		}
382
383		/// This method only exists for migration purposes and will be deleted once the
384		/// foreign assets migration is finished.
385		pub fn register_foreign_asset(
386			asset_id: AssetId,
387			xcm_location: Location,
388			decimals: u8,
389			symbol: BoundedVec<u8, ConstU32<256>>,
390			name: BoundedVec<u8, ConstU32<256>>,
391		) -> DispatchResult {
392			Self::do_create_asset(asset_id, xcm_location, decimals, symbol, name, None)
393		}
394
395		/// Mint an asset into a specific account
396		pub fn mint_into(
397			asset_id: AssetId,
398			beneficiary: T::AccountId,
399			amount: U256,
400		) -> Result<(), evm::EvmError> {
401			// We perform the evm call in a storage transaction to ensure that if it fail
402			// any contract storage changes are rolled back.
403			frame_support::storage::with_storage_layer(|| {
404				EvmCaller::<T>::erc20_mint_into(
405					Self::contract_address_from_asset_id(asset_id),
406					T::AccountIdToH160::convert(beneficiary),
407					amount,
408				)
409			})
410			.map_err(Into::into)
411		}
412
413		/// Transfer an asset from an account to another one
414		pub fn transfer(
415			asset_id: AssetId,
416			from: T::AccountId,
417			to: T::AccountId,
418			amount: U256,
419		) -> Result<(), evm::EvmError> {
420			frame_support::storage::with_storage_layer(|| {
421				EvmCaller::<T>::erc20_transfer(
422					Self::contract_address_from_asset_id(asset_id),
423					T::AccountIdToH160::convert(from),
424					T::AccountIdToH160::convert(to),
425					amount,
426				)
427			})
428			.map_err(Into::into)
429		}
430
431		pub fn balance(asset_id: AssetId, who: T::AccountId) -> Result<U256, evm::EvmError> {
432			EvmCaller::<T>::erc20_balance_of(asset_id, T::AccountIdToH160::convert(who))
433				.map_err(Into::into)
434		}
435
436		/// Approve a spender to spend a certain amount of tokens from the owner account
437		pub fn approve(
438			asset_id: AssetId,
439			owner: T::AccountId,
440			spender: T::AccountId,
441			amount: U256,
442		) -> Result<(), evm::EvmError> {
443			// We perform the evm call in a storage transaction to ensure that if it fail
444			// any contract storage changes are rolled back.
445			frame_support::storage::with_storage_layer(|| {
446				EvmCaller::<T>::erc20_approve(
447					Self::contract_address_from_asset_id(asset_id),
448					T::AccountIdToH160::convert(owner),
449					T::AccountIdToH160::convert(spender),
450					amount,
451				)
452			})
453			.map_err(Into::into)
454		}
455
456		pub fn weight_of_erc20_burn() -> Weight {
457			T::GasWeightMapping::gas_to_weight(evm::ERC20_BURN_FROM_GAS_LIMIT, true)
458		}
459		pub fn weight_of_erc20_mint() -> Weight {
460			T::GasWeightMapping::gas_to_weight(evm::ERC20_MINT_INTO_GAS_LIMIT, true)
461		}
462		pub fn weight_of_erc20_transfer() -> Weight {
463			T::GasWeightMapping::gas_to_weight(evm::ERC20_TRANSFER_GAS_LIMIT, true)
464		}
465		#[cfg(feature = "runtime-benchmarks")]
466		pub fn set_asset(asset_location: Location, asset_id: AssetId) {
467			AssetsByLocation::<T>::insert(&asset_location, (asset_id, AssetStatus::Active));
468			AssetsById::<T>::insert(&asset_id, asset_location);
469		}
470	}
471
472	#[pallet::call]
473	impl<T: Config> Pallet<T> {
474		/// Create new asset with the ForeignAssetCreator
475		#[pallet::call_index(0)]
476		#[pallet::weight(<T as Config>::WeightInfo::create_foreign_asset())]
477		pub fn create_foreign_asset(
478			origin: OriginFor<T>,
479			asset_id: AssetId,
480			asset_xcm_location: Location,
481			decimals: u8,
482			symbol: BoundedVec<u8, ConstU32<256>>,
483			name: BoundedVec<u8, ConstU32<256>>,
484		) -> DispatchResult {
485			let origin_type = T::ForeignAssetCreatorOrigin::ensure_origin(origin.clone())?;
486
487			Self::ensure_origin_can_modify_location(origin_type.clone(), &asset_xcm_location)?;
488			let deposit_account = Self::get_deposit_account(origin_type)?;
489
490			Self::do_create_asset(
491				asset_id,
492				asset_xcm_location,
493				decimals,
494				symbol,
495				name,
496				deposit_account,
497			)
498		}
499
500		/// Change the xcm type mapping for a given assetId
501		/// We also change this if the previous units per second where pointing at the old
502		/// assetType
503		#[pallet::call_index(1)]
504		#[pallet::weight(<T as Config>::WeightInfo::change_xcm_location())]
505		pub fn change_xcm_location(
506			origin: OriginFor<T>,
507			asset_id: AssetId,
508			new_xcm_location: Location,
509		) -> DispatchResult {
510			let origin_type = T::ForeignAssetModifierOrigin::ensure_origin(origin.clone())?;
511
512			Self::ensure_origin_can_modify_location(origin_type.clone(), &new_xcm_location)?;
513
514			let previous_location =
515				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
516
517			Self::ensure_origin_can_modify_location(origin_type, &previous_location)?;
518
519			Self::do_change_xcm_location(asset_id, previous_location, new_xcm_location)
520		}
521
522		/// Freeze a given foreign assetId
523		#[pallet::call_index(2)]
524		#[pallet::weight(<T as Config>::WeightInfo::freeze_foreign_asset())]
525		pub fn freeze_foreign_asset(
526			origin: OriginFor<T>,
527			asset_id: AssetId,
528			allow_xcm_deposit: bool,
529		) -> DispatchResult {
530			let origin_type = T::ForeignAssetFreezerOrigin::ensure_origin(origin.clone())?;
531
532			let xcm_location =
533				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
534
535			Self::ensure_origin_can_modify_location(origin_type, &xcm_location)?;
536
537			Self::do_freeze_asset(asset_id, xcm_location, allow_xcm_deposit)
538		}
539
540		/// Unfreeze a given foreign assetId
541		#[pallet::call_index(3)]
542		#[pallet::weight(<T as Config>::WeightInfo::unfreeze_foreign_asset())]
543		pub fn unfreeze_foreign_asset(origin: OriginFor<T>, asset_id: AssetId) -> DispatchResult {
544			let origin_type = T::ForeignAssetUnfreezerOrigin::ensure_origin(origin.clone())?;
545
546			let xcm_location =
547				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
548
549			Self::ensure_origin_can_modify_location(origin_type, &xcm_location)?;
550
551			Self::do_unfreeze_asset(asset_id, xcm_location)
552		}
553	}
554
555	impl<T: Config> Pallet<T> {
556		/// Ensure that the caller origin can modify the location,
557		fn ensure_origin_can_modify_location(
558			origin_type: OriginType,
559			location: &Location,
560		) -> DispatchResult {
561			match origin_type {
562				OriginType::XCM(origin_location) => {
563					ensure!(
564						location.starts_with(&origin_location),
565						Error::<T>::LocationOutsideOfOrigin,
566					);
567				}
568				OriginType::Governance => {
569					// nothing to check Governance can change any asset
570				}
571			};
572			Ok(())
573		}
574
575		fn get_deposit_account(
576			origin_type: OriginType,
577		) -> Result<Option<T::AccountId>, DispatchError> {
578			match origin_type {
579				OriginType::XCM(origin_location) => {
580					let deposit_account = convert_location::<T>(&origin_location)?;
581					Ok(Some(deposit_account))
582				}
583				OriginType::Governance => Ok(None),
584			}
585		}
586
587		pub fn do_create_asset(
588			asset_id: AssetId,
589			asset_xcm_location: Location,
590			decimals: u8,
591			symbol: BoundedVec<u8, ConstU32<256>>,
592			name: BoundedVec<u8, ConstU32<256>>,
593			deposit_account: Option<T::AccountId>,
594		) -> DispatchResult {
595			ensure!(
596				!AssetsById::<T>::contains_key(&asset_id),
597				Error::<T>::AssetAlreadyExists
598			);
599
600			ensure!(
601				!AssetsByLocation::<T>::contains_key(&asset_xcm_location),
602				Error::<T>::LocationAlreadyExists
603			);
604
605			ensure!(
606				AssetsById::<T>::count() < T::MaxForeignAssets::get(),
607				Error::<T>::TooManyForeignAssets
608			);
609
610			ensure!(
611				T::AssetIdFilter::contains(&asset_id),
612				Error::<T>::AssetIdFiltered
613			);
614
615			let symbol = core::str::from_utf8(&symbol).map_err(|_| Error::<T>::InvalidSymbol)?;
616			let name = core::str::from_utf8(&name).map_err(|_| Error::<T>::InvalidTokenName)?;
617			let contract_address = EvmCaller::<T>::erc20_create(asset_id, decimals, symbol, name)?;
618
619			let deposit = if let Some(deposit_account) = deposit_account {
620				let deposit = T::ForeignAssetCreationDeposit::get();
621
622				// Reserve _deposit_ amount of funds from the caller
623				<T as Config>::Currency::reserve(&deposit_account, deposit)?;
624
625				// Insert the amount that is reserved from the user
626				AssetsCreationDetails::<T>::insert(
627					&asset_id,
628					AssetDepositDetails {
629						deposit_account,
630						deposit,
631					},
632				);
633
634				Some(deposit)
635			} else {
636				None
637			};
638
639			// Insert the association assetId->foreigAsset
640			// Insert the association foreigAsset->assetId
641			AssetsById::<T>::insert(&asset_id, &asset_xcm_location);
642			AssetsByLocation::<T>::insert(&asset_xcm_location, (asset_id, AssetStatus::Active));
643
644			T::OnForeignAssetCreated::on_asset_created(&asset_xcm_location, &asset_id);
645
646			Self::deposit_event(Event::ForeignAssetCreated {
647				contract_address,
648				asset_id,
649				xcm_location: asset_xcm_location,
650				deposit,
651			});
652			Ok(())
653		}
654
655		pub fn do_change_xcm_location(
656			asset_id: AssetId,
657			previous_xcm_location: Location,
658			new_xcm_location: Location,
659		) -> DispatchResult {
660			ensure!(
661				!AssetsByLocation::<T>::contains_key(&new_xcm_location),
662				Error::<T>::LocationAlreadyExists
663			);
664
665			// Remove previous foreign asset info
666			let (_asset_id, asset_status) = AssetsByLocation::<T>::take(&previous_xcm_location)
667				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
668
669			// Insert new foreign asset info
670			AssetsById::<T>::insert(&asset_id, &new_xcm_location);
671			AssetsByLocation::<T>::insert(&new_xcm_location, (asset_id, asset_status));
672
673			Self::deposit_event(Event::ForeignAssetXcmLocationChanged {
674				asset_id,
675				new_xcm_location,
676				previous_xcm_location,
677			});
678			Ok(())
679		}
680
681		pub fn do_freeze_asset(
682			asset_id: AssetId,
683			xcm_location: Location,
684			allow_xcm_deposit: bool,
685		) -> DispatchResult {
686			let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
687				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
688
689			ensure!(
690				asset_status == AssetStatus::Active,
691				Error::<T>::AssetAlreadyFrozen
692			);
693
694			EvmCaller::<T>::erc20_pause(asset_id)?;
695
696			let new_asset_status = if allow_xcm_deposit {
697				AssetStatus::FrozenXcmDepositAllowed
698			} else {
699				AssetStatus::FrozenXcmDepositForbidden
700			};
701
702			AssetsByLocation::<T>::insert(&xcm_location, (asset_id, new_asset_status));
703
704			Self::deposit_event(Event::ForeignAssetFrozen {
705				asset_id,
706				xcm_location,
707			});
708			Ok(())
709		}
710
711		pub fn do_unfreeze_asset(asset_id: AssetId, xcm_location: Location) -> DispatchResult {
712			let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
713				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
714
715			ensure!(
716				asset_status == AssetStatus::FrozenXcmDepositAllowed
717					|| asset_status == AssetStatus::FrozenXcmDepositForbidden,
718				Error::<T>::AssetNotFrozen
719			);
720
721			EvmCaller::<T>::erc20_unpause(asset_id)?;
722
723			AssetsByLocation::<T>::insert(&xcm_location, (asset_id, AssetStatus::Active));
724
725			Self::deposit_event(Event::ForeignAssetUnfrozen {
726				asset_id,
727				xcm_location,
728			});
729			Ok(())
730		}
731	}
732
733	impl<T: Config> xcm_executor::traits::TransactAsset for Pallet<T> {
734		// For optimization reasons, the asset we want to deposit has not really been withdrawn,
735		// we have just traced from which account it should have been withdrawn.
736		// So we will retrieve these information and make the transfer from the origin account.
737		fn deposit_asset(what: &Asset, who: &Location, _context: Option<&XcmContext>) -> XcmResult {
738			let (contract_address, amount, asset_status) =
739				ForeignAssetsMatcher::<T>::match_asset(what)?;
740
741			if let AssetStatus::FrozenXcmDepositForbidden = asset_status {
742				return Err(MatchError::AssetNotHandled.into());
743			}
744
745			let beneficiary = T::XcmLocationToH160::convert_location(who)
746				.ok_or(MatchError::AccountIdConversionFailed)?;
747
748			// We perform the evm transfers in a storage transaction to ensure that if it fail
749			// any contract storage changes are rolled back.
750			frame_support::storage::with_storage_layer(|| {
751				EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount)
752			})?;
753
754			Ok(())
755		}
756
757		fn internal_transfer_asset(
758			asset: &Asset,
759			from: &Location,
760			to: &Location,
761			_context: &XcmContext,
762		) -> Result<AssetsInHolding, XcmError> {
763			let (contract_address, amount, asset_status) =
764				ForeignAssetsMatcher::<T>::match_asset(asset)?;
765
766			if let AssetStatus::FrozenXcmDepositForbidden | AssetStatus::FrozenXcmDepositAllowed =
767				asset_status
768			{
769				return Err(MatchError::AssetNotHandled.into());
770			}
771
772			let from = T::XcmLocationToH160::convert_location(from)
773				.ok_or(MatchError::AccountIdConversionFailed)?;
774
775			let to = T::XcmLocationToH160::convert_location(to)
776				.ok_or(MatchError::AccountIdConversionFailed)?;
777
778			// We perform the evm transfers in a storage transaction to ensure that if it fail
779			// any contract storage changes are rolled back.
780			frame_support::storage::with_storage_layer(|| {
781				EvmCaller::<T>::erc20_transfer(contract_address, from, to, amount)
782			})?;
783
784			Ok(asset.clone().into())
785		}
786
787		// Since we don't control the erc20 contract that manages the asset we want to withdraw,
788		// we can't really withdraw this asset, we can only transfer it to another account.
789		// It would be possible to transfer the asset to a dedicated account that would reflect
790		// the content of the xcm holding, but this would imply to perform two evm calls instead of
791		// one (1 to withdraw the asset and a second one to deposit it).
792		// In order to perform only one evm call, we just trace the origin of the asset,
793		// and then the transfer will only really be performed in the deposit instruction.
794		fn withdraw_asset(
795			what: &Asset,
796			who: &Location,
797			_context: Option<&XcmContext>,
798		) -> Result<AssetsInHolding, XcmError> {
799			let (contract_address, amount, asset_status) =
800				ForeignAssetsMatcher::<T>::match_asset(what)?;
801			let who = T::XcmLocationToH160::convert_location(who)
802				.ok_or(MatchError::AccountIdConversionFailed)?;
803
804			if let AssetStatus::FrozenXcmDepositForbidden | AssetStatus::FrozenXcmDepositAllowed =
805				asset_status
806			{
807				return Err(MatchError::AssetNotHandled.into());
808			}
809
810			// We perform the evm transfers in a storage transaction to ensure that if it fail
811			// any contract storage changes are rolled back.
812			frame_support::storage::with_storage_layer(|| {
813				EvmCaller::<T>::erc20_burn_from(contract_address, who, amount)
814			})?;
815
816			Ok(what.clone().into())
817		}
818	}
819
820	impl<T: Config> sp_runtime::traits::MaybeEquivalence<Location, AssetId> for Pallet<T> {
821		fn convert(location: &Location) -> Option<AssetId> {
822			AssetsByLocation::<T>::get(location).map(|(asset_id, _)| asset_id)
823		}
824		fn convert_back(asset_id: &AssetId) -> Option<Location> {
825			AssetsById::<T>::get(asset_id)
826		}
827	}
828}