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		#[cfg(feature = "runtime-benchmarks")]
472		pub fn create_asset_contract(
473			asset_id: AssetId,
474			decimals: u8,
475			symbol: &str,
476			name: &str,
477		) -> Result<H160, Error<T>> {
478			EvmCaller::<T>::erc20_create(asset_id, decimals, symbol, name)
479		}
480	}
481
482	#[pallet::call]
483	impl<T: Config> Pallet<T> {
484		/// Create new asset with the ForeignAssetCreator
485		#[pallet::call_index(0)]
486		#[pallet::weight(<T as Config>::WeightInfo::create_foreign_asset())]
487		pub fn create_foreign_asset(
488			origin: OriginFor<T>,
489			asset_id: AssetId,
490			asset_xcm_location: Location,
491			decimals: u8,
492			symbol: BoundedVec<u8, ConstU32<256>>,
493			name: BoundedVec<u8, ConstU32<256>>,
494		) -> DispatchResult {
495			let origin_type = T::ForeignAssetCreatorOrigin::ensure_origin(origin.clone())?;
496
497			Self::ensure_origin_can_modify_location(origin_type.clone(), &asset_xcm_location)?;
498			let deposit_account = Self::get_deposit_account(origin_type)?;
499
500			Self::do_create_asset(
501				asset_id,
502				asset_xcm_location,
503				decimals,
504				symbol,
505				name,
506				deposit_account,
507			)
508		}
509
510		/// Change the xcm type mapping for a given assetId
511		/// We also change this if the previous units per second where pointing at the old
512		/// assetType
513		#[pallet::call_index(1)]
514		#[pallet::weight(<T as Config>::WeightInfo::change_xcm_location())]
515		pub fn change_xcm_location(
516			origin: OriginFor<T>,
517			asset_id: AssetId,
518			new_xcm_location: Location,
519		) -> DispatchResult {
520			let origin_type = T::ForeignAssetModifierOrigin::ensure_origin(origin.clone())?;
521
522			Self::ensure_origin_can_modify_location(origin_type.clone(), &new_xcm_location)?;
523
524			let previous_location =
525				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
526
527			Self::ensure_origin_can_modify_location(origin_type, &previous_location)?;
528
529			Self::do_change_xcm_location(asset_id, previous_location, new_xcm_location)
530		}
531
532		/// Freeze a given foreign assetId
533		#[pallet::call_index(2)]
534		#[pallet::weight(<T as Config>::WeightInfo::freeze_foreign_asset())]
535		pub fn freeze_foreign_asset(
536			origin: OriginFor<T>,
537			asset_id: AssetId,
538			allow_xcm_deposit: bool,
539		) -> DispatchResult {
540			let origin_type = T::ForeignAssetFreezerOrigin::ensure_origin(origin.clone())?;
541
542			let xcm_location =
543				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
544
545			Self::ensure_origin_can_modify_location(origin_type, &xcm_location)?;
546
547			Self::do_freeze_asset(asset_id, xcm_location, allow_xcm_deposit)
548		}
549
550		/// Unfreeze a given foreign assetId
551		#[pallet::call_index(3)]
552		#[pallet::weight(<T as Config>::WeightInfo::unfreeze_foreign_asset())]
553		pub fn unfreeze_foreign_asset(origin: OriginFor<T>, asset_id: AssetId) -> DispatchResult {
554			let origin_type = T::ForeignAssetUnfreezerOrigin::ensure_origin(origin.clone())?;
555
556			let xcm_location =
557				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
558
559			Self::ensure_origin_can_modify_location(origin_type, &xcm_location)?;
560
561			Self::do_unfreeze_asset(asset_id, xcm_location)
562		}
563	}
564
565	impl<T: Config> Pallet<T> {
566		/// Ensure that the caller origin can modify the location,
567		fn ensure_origin_can_modify_location(
568			origin_type: OriginType,
569			location: &Location,
570		) -> DispatchResult {
571			match origin_type {
572				OriginType::XCM(origin_location) => {
573					ensure!(
574						location.starts_with(&origin_location),
575						Error::<T>::LocationOutsideOfOrigin,
576					);
577				}
578				OriginType::Governance => {
579					// nothing to check Governance can change any asset
580				}
581			};
582			Ok(())
583		}
584
585		fn get_deposit_account(
586			origin_type: OriginType,
587		) -> Result<Option<T::AccountId>, DispatchError> {
588			match origin_type {
589				OriginType::XCM(origin_location) => {
590					let deposit_account = convert_location::<T>(&origin_location)?;
591					Ok(Some(deposit_account))
592				}
593				OriginType::Governance => Ok(None),
594			}
595		}
596
597		pub fn do_create_asset(
598			asset_id: AssetId,
599			asset_xcm_location: Location,
600			decimals: u8,
601			symbol: BoundedVec<u8, ConstU32<256>>,
602			name: BoundedVec<u8, ConstU32<256>>,
603			deposit_account: Option<T::AccountId>,
604		) -> DispatchResult {
605			ensure!(
606				!AssetsById::<T>::contains_key(&asset_id),
607				Error::<T>::AssetAlreadyExists
608			);
609
610			ensure!(
611				!AssetsByLocation::<T>::contains_key(&asset_xcm_location),
612				Error::<T>::LocationAlreadyExists
613			);
614
615			ensure!(
616				AssetsById::<T>::count() < T::MaxForeignAssets::get(),
617				Error::<T>::TooManyForeignAssets
618			);
619
620			ensure!(
621				T::AssetIdFilter::contains(&asset_id),
622				Error::<T>::AssetIdFiltered
623			);
624
625			let symbol = core::str::from_utf8(&symbol).map_err(|_| Error::<T>::InvalidSymbol)?;
626			let name = core::str::from_utf8(&name).map_err(|_| Error::<T>::InvalidTokenName)?;
627			let contract_address = EvmCaller::<T>::erc20_create(asset_id, decimals, symbol, name)?;
628
629			let deposit = if let Some(deposit_account) = deposit_account {
630				let deposit = T::ForeignAssetCreationDeposit::get();
631
632				// Reserve _deposit_ amount of funds from the caller
633				<T as Config>::Currency::reserve(&deposit_account, deposit)?;
634
635				// Insert the amount that is reserved from the user
636				AssetsCreationDetails::<T>::insert(
637					&asset_id,
638					AssetDepositDetails {
639						deposit_account,
640						deposit,
641					},
642				);
643
644				Some(deposit)
645			} else {
646				None
647			};
648
649			// Insert the association assetId->foreigAsset
650			// Insert the association foreigAsset->assetId
651			AssetsById::<T>::insert(&asset_id, &asset_xcm_location);
652			AssetsByLocation::<T>::insert(&asset_xcm_location, (asset_id, AssetStatus::Active));
653
654			T::OnForeignAssetCreated::on_asset_created(&asset_xcm_location, &asset_id);
655
656			Self::deposit_event(Event::ForeignAssetCreated {
657				contract_address,
658				asset_id,
659				xcm_location: asset_xcm_location,
660				deposit,
661			});
662			Ok(())
663		}
664
665		pub fn do_change_xcm_location(
666			asset_id: AssetId,
667			previous_xcm_location: Location,
668			new_xcm_location: Location,
669		) -> DispatchResult {
670			ensure!(
671				!AssetsByLocation::<T>::contains_key(&new_xcm_location),
672				Error::<T>::LocationAlreadyExists
673			);
674
675			// Remove previous foreign asset info
676			let (_asset_id, asset_status) = AssetsByLocation::<T>::take(&previous_xcm_location)
677				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
678
679			// Insert new foreign asset info
680			AssetsById::<T>::insert(&asset_id, &new_xcm_location);
681			AssetsByLocation::<T>::insert(&new_xcm_location, (asset_id, asset_status));
682
683			Self::deposit_event(Event::ForeignAssetXcmLocationChanged {
684				asset_id,
685				new_xcm_location,
686				previous_xcm_location,
687			});
688			Ok(())
689		}
690
691		pub fn do_freeze_asset(
692			asset_id: AssetId,
693			xcm_location: Location,
694			allow_xcm_deposit: bool,
695		) -> DispatchResult {
696			let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
697				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
698
699			ensure!(
700				asset_status == AssetStatus::Active,
701				Error::<T>::AssetAlreadyFrozen
702			);
703
704			EvmCaller::<T>::erc20_pause(asset_id)?;
705
706			let new_asset_status = if allow_xcm_deposit {
707				AssetStatus::FrozenXcmDepositAllowed
708			} else {
709				AssetStatus::FrozenXcmDepositForbidden
710			};
711
712			AssetsByLocation::<T>::insert(&xcm_location, (asset_id, new_asset_status));
713
714			Self::deposit_event(Event::ForeignAssetFrozen {
715				asset_id,
716				xcm_location,
717			});
718			Ok(())
719		}
720
721		pub fn do_unfreeze_asset(asset_id: AssetId, xcm_location: Location) -> DispatchResult {
722			let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
723				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
724
725			ensure!(
726				asset_status == AssetStatus::FrozenXcmDepositAllowed
727					|| asset_status == AssetStatus::FrozenXcmDepositForbidden,
728				Error::<T>::AssetNotFrozen
729			);
730
731			EvmCaller::<T>::erc20_unpause(asset_id)?;
732
733			AssetsByLocation::<T>::insert(&xcm_location, (asset_id, AssetStatus::Active));
734
735			Self::deposit_event(Event::ForeignAssetUnfrozen {
736				asset_id,
737				xcm_location,
738			});
739			Ok(())
740		}
741	}
742
743	impl<T: Config> xcm_executor::traits::TransactAsset for Pallet<T> {
744		// For optimization reasons, the asset we want to deposit has not really been withdrawn,
745		// we have just traced from which account it should have been withdrawn.
746		// So we will retrieve these information and make the transfer from the origin account.
747		fn deposit_asset(what: &Asset, who: &Location, _context: Option<&XcmContext>) -> XcmResult {
748			let (contract_address, amount, asset_status) =
749				ForeignAssetsMatcher::<T>::match_asset(what)?;
750
751			if let AssetStatus::FrozenXcmDepositForbidden = asset_status {
752				return Err(MatchError::AssetNotHandled.into());
753			}
754
755			let beneficiary = T::XcmLocationToH160::convert_location(who)
756				.ok_or(MatchError::AccountIdConversionFailed)?;
757
758			// We perform the evm transfers in a storage transaction to ensure that if it fail
759			// any contract storage changes are rolled back.
760			frame_support::storage::with_storage_layer(|| {
761				EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount)
762			})?;
763
764			Ok(())
765		}
766
767		fn internal_transfer_asset(
768			asset: &Asset,
769			from: &Location,
770			to: &Location,
771			_context: &XcmContext,
772		) -> Result<AssetsInHolding, XcmError> {
773			let (contract_address, amount, asset_status) =
774				ForeignAssetsMatcher::<T>::match_asset(asset)?;
775
776			if let AssetStatus::FrozenXcmDepositForbidden | AssetStatus::FrozenXcmDepositAllowed =
777				asset_status
778			{
779				return Err(MatchError::AssetNotHandled.into());
780			}
781
782			let from = T::XcmLocationToH160::convert_location(from)
783				.ok_or(MatchError::AccountIdConversionFailed)?;
784
785			let to = T::XcmLocationToH160::convert_location(to)
786				.ok_or(MatchError::AccountIdConversionFailed)?;
787
788			// We perform the evm transfers in a storage transaction to ensure that if it fail
789			// any contract storage changes are rolled back.
790			frame_support::storage::with_storage_layer(|| {
791				EvmCaller::<T>::erc20_transfer(contract_address, from, to, amount)
792			})?;
793
794			Ok(asset.clone().into())
795		}
796
797		// Since we don't control the erc20 contract that manages the asset we want to withdraw,
798		// we can't really withdraw this asset, we can only transfer it to another account.
799		// It would be possible to transfer the asset to a dedicated account that would reflect
800		// the content of the xcm holding, but this would imply to perform two evm calls instead of
801		// one (1 to withdraw the asset and a second one to deposit it).
802		// In order to perform only one evm call, we just trace the origin of the asset,
803		// and then the transfer will only really be performed in the deposit instruction.
804		fn withdraw_asset(
805			what: &Asset,
806			who: &Location,
807			_context: Option<&XcmContext>,
808		) -> Result<AssetsInHolding, XcmError> {
809			let (contract_address, amount, asset_status) =
810				ForeignAssetsMatcher::<T>::match_asset(what)?;
811			let who = T::XcmLocationToH160::convert_location(who)
812				.ok_or(MatchError::AccountIdConversionFailed)?;
813
814			if let AssetStatus::FrozenXcmDepositForbidden | AssetStatus::FrozenXcmDepositAllowed =
815				asset_status
816			{
817				return Err(MatchError::AssetNotHandled.into());
818			}
819
820			// We perform the evm transfers in a storage transaction to ensure that if it fail
821			// any contract storage changes are rolled back.
822			frame_support::storage::with_storage_layer(|| {
823				EvmCaller::<T>::erc20_burn_from(contract_address, who, amount)
824			})?;
825
826			Ok(what.clone().into())
827		}
828
829		#[cfg(feature = "runtime-benchmarks")]
830		fn can_check_out(_dest: &Location, _what: &Asset, _context: &XcmContext) -> XcmResult {
831			// Needed for the benchmarks to work
832			Ok(())
833		}
834
835		#[cfg(feature = "runtime-benchmarks")]
836		fn check_out(_dest: &Location, _what: &Asset, _context: &XcmContext) {
837			// Needed for benchmarks to work
838		}
839	}
840
841	impl<T: Config> sp_runtime::traits::MaybeEquivalence<Location, AssetId> for Pallet<T> {
842		fn convert(location: &Location) -> Option<AssetId> {
843			AssetsByLocation::<T>::get(location).map(|(asset_id, _)| asset_id)
844		}
845		fn convert_back(asset_id: &AssetId) -> Option<Location> {
846			AssetsById::<T>::get(asset_id)
847		}
848	}
849}