1
// Copyright 2024 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 beetween 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"))]
41
pub mod benchmarks;
42
#[cfg(test)]
43
pub mod mock;
44
#[cfg(test)]
45
pub mod tests;
46
pub mod weights;
47

            
48
mod evm;
49

            
50
pub use pallet::*;
51
pub use weights::WeightInfo;
52

            
53
use self::evm::EvmCaller;
54
use ethereum_types::{H160, U256};
55
use frame_support::pallet;
56
use frame_support::pallet_prelude::*;
57
use frame_support::traits::Contains;
58
use frame_system::pallet_prelude::*;
59
use xcm::latest::{
60
	Asset, AssetId as XcmAssetId, Error as XcmError, Fungibility, Location, Result as XcmResult,
61
	XcmContext,
62
};
63
use xcm_executor::traits::Error as MatchError;
64

            
65
const FOREIGN_ASSETS_PREFIX: [u8; 4] = [0xff, 0xff, 0xff, 0xff];
66

            
67
/// Trait for the OnForeignAssetRegistered hook
68
pub trait ForeignAssetCreatedHook<ForeignAsset> {
69
	fn on_asset_created(foreign_asset: &ForeignAsset, asset_id: &AssetId);
70
}
71

            
72
impl<ForeignAsset> ForeignAssetCreatedHook<ForeignAsset> for () {
73
20
	fn on_asset_created(_foreign_asset: &ForeignAsset, _asset_id: &AssetId) {}
74
}
75

            
76
pub(crate) struct ForeignAssetsMatcher<T>(core::marker::PhantomData<T>);
77

            
78
impl<T: crate::Config> ForeignAssetsMatcher<T> {
79
52
	fn match_asset(asset: &Asset) -> Result<(H160, U256, AssetStatus), MatchError> {
80
52
		let (amount, location) = match (&asset.fun, &asset.id) {
81
52
			(Fungibility::Fungible(ref amount), XcmAssetId(ref location)) => (amount, location),
82
			_ => return Err(MatchError::AssetNotHandled),
83
		};
84

            
85
52
		if let Some((asset_id, asset_status)) = AssetsByLocation::<T>::get(&location) {
86
18
			Ok((
87
18
				Pallet::<T>::contract_address_from_asset_id(asset_id),
88
18
				U256::from(*amount),
89
18
				asset_status,
90
18
			))
91
		} else {
92
34
			Err(MatchError::AssetNotHandled)
93
		}
94
52
	}
95
}
96

            
97
207
#[derive(Decode, Debug, Encode, PartialEq, TypeInfo)]
98
pub enum AssetStatus {
99
28
	/// All operations are enabled
100
	Active,
101
5
	/// The asset is frozen, but deposit from XCM still work
102
	FrozenXcmDepositAllowed,
103
	/// The asset is frozen, and deposit from XCM will fail
104
	FrozenXcmDepositForbidden,
105
}
106

            
107
808
#[pallet]
108
pub mod pallet {
109
	use super::*;
110
	use pallet_evm::{GasWeightMapping, Runner};
111
	use sp_runtime::traits::{AccountIdConversion, Convert};
112
	use xcm_executor::traits::ConvertLocation;
113
	use xcm_executor::traits::Error as MatchError;
114
	use xcm_executor::AssetsInHolding;
115

            
116
71
	#[pallet::pallet]
117
	#[pallet::without_storage_info]
118
	pub struct Pallet<T>(PhantomData<T>);
119

            
120
	/// The moonbeam foreign assets's pallet id
121
	pub const PALLET_ID: frame_support::PalletId = frame_support::PalletId(*b"forgasst");
122

            
123
	#[pallet::config]
124
	pub trait Config: frame_system::Config + pallet_evm::Config {
125
		// Convert AccountId to H160
126
		type AccountIdToH160: Convert<Self::AccountId, H160>;
127

            
128
		/// A filter to forbid some AssetId values, if you don't use it, put "Everything"
129
		type AssetIdFilter: Contains<AssetId>;
130

            
131
		/// EVM runner
132
		type EvmRunner: Runner<Self>;
133

            
134
		/// Origin that is allowed to create a new foreign assets
135
		type ForeignAssetCreatorOrigin: EnsureOrigin<Self::RuntimeOrigin>;
136

            
137
		/// Origin that is allowed to freeze all tokens of a foreign asset
138
		type ForeignAssetFreezerOrigin: EnsureOrigin<Self::RuntimeOrigin>;
139

            
140
		/// Origin that is allowed to modify asset information for foreign assets
141
		type ForeignAssetModifierOrigin: EnsureOrigin<Self::RuntimeOrigin>;
142

            
143
		/// Origin that is allowed to unfreeze all tokens of a foreign asset that was previously
144
		/// frozen
145
		type ForeignAssetUnfreezerOrigin: EnsureOrigin<Self::RuntimeOrigin>;
146

            
147
		/// Hook to be called when new foreign asset is registered.
148
		type OnForeignAssetCreated: ForeignAssetCreatedHook<Location>;
149

            
150
		/// Maximum nulmbers of differnt foreign assets
151
		type MaxForeignAssets: Get<u32>;
152

            
153
		/// The overarching event type.
154
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
155

            
156
		/// Weight information for extrinsics in this pallet.
157
		type WeightInfo: WeightInfo;
158

            
159
		// Convert XCM Location to H160
160
		type XcmLocationToH160: ConvertLocation<H160>;
161
	}
162

            
163
	pub type AssetBalance = U256;
164
	pub type AssetId = u128;
165

            
166
	/// An error that can occur while executing the mapping pallet's logic.
167
12
	#[pallet::error]
168
	pub enum Error<T> {
169
		AssetAlreadyExists,
170
		AssetAlreadyFrozen,
171
		AssetDoesNotExist,
172
		AssetIdFiltered,
173
		AssetNotFrozen,
174
		CorruptedStorageOrphanLocation,
175
		//Erc20ContractCallFail,
176
		Erc20ContractCreationFail,
177
		EvmCallPauseFail,
178
		EvmCallUnpauseFail,
179
		EvmInternalError,
180
		InvalidSymbol,
181
		InvalidTokenName,
182
		LocationAlreadyExists,
183
		TooManyForeignAssets,
184
	}
185

            
186
	#[pallet::event]
187
30
	#[pallet::generate_deposit(pub(crate) fn deposit_event)]
188
	pub enum Event<T: Config> {
189
2
		/// New asset with the asset manager is registered
190
		ForeignAssetCreated {
191
			contract_address: H160,
192
			asset_id: AssetId,
193
			xcm_location: Location,
194
		},
195
1
		/// Changed the xcm type mapping for a given asset id
196
		ForeignAssetXcmLocationChanged {
197
			asset_id: AssetId,
198
			new_xcm_location: Location,
199
		},
200
		// Freezes all tokens of a given asset id
201
		ForeignAssetFrozen {
202
			asset_id: AssetId,
203
			xcm_location: Location,
204
		},
205
		// Thawing a previously frozen asset
206
		ForeignAssetUnfrozen {
207
			asset_id: AssetId,
208
			xcm_location: Location,
209
		},
210
	}
211

            
212
	/// Mapping from an asset id to a Foreign asset type.
213
	/// This is mostly used when receiving transaction specifying an asset directly,
214
	/// like transferring an asset from this chain to another.
215
251
	#[pallet::storage]
216
	#[pallet::getter(fn assets_by_id)]
217
	pub type AssetsById<T: Config> =
218
		CountedStorageMap<_, Blake2_128Concat, AssetId, Location, OptionQuery>;
219

            
220
	/// Reverse mapping of AssetsById. Mapping from a foreign asset to an asset id.
221
	/// This is mostly used when receiving a multilocation XCM message to retrieve
222
	/// the corresponding asset in which tokens should me minted.
223
135
	#[pallet::storage]
224
	#[pallet::getter(fn assets_by_location)]
225
	pub type AssetsByLocation<T: Config> =
226
		StorageMap<_, Blake2_128Concat, Location, (AssetId, AssetStatus)>;
227

            
228
	impl<T: Config> Pallet<T> {
229
		/// The account ID of this pallet
230
		#[inline]
231
87
		pub fn account_id() -> H160 {
232
87
			let account_id: T::AccountId = PALLET_ID.into_account_truncating();
233
87
			T::AccountIdToH160::convert(account_id)
234
87
		}
235

            
236
		/// Compute asset contract address from asset id
237
		#[inline]
238
88
		pub fn contract_address_from_asset_id(asset_id: AssetId) -> H160 {
239
88
			let mut buffer = [0u8; 20];
240
88
			buffer[..4].copy_from_slice(&FOREIGN_ASSETS_PREFIX);
241
88
			buffer[4..].copy_from_slice(&asset_id.to_be_bytes());
242
88
			H160(buffer)
243
88
		}
244

            
245
27
		pub fn register_foreign_asset(
246
27
			asset_id: AssetId,
247
27
			xcm_location: Location,
248
27
			decimals: u8,
249
27
			symbol: BoundedVec<u8, ConstU32<256>>,
250
27
			name: BoundedVec<u8, ConstU32<256>>,
251
27
		) -> DispatchResult {
252
27
			// Ensure such an assetId does not exist
253
27
			ensure!(
254
27
				!AssetsById::<T>::contains_key(&asset_id),
255
1
				Error::<T>::AssetAlreadyExists
256
			);
257

            
258
26
			ensure!(
259
26
				!AssetsByLocation::<T>::contains_key(&xcm_location),
260
1
				Error::<T>::LocationAlreadyExists
261
			);
262

            
263
25
			ensure!(
264
25
				AssetsById::<T>::count() < T::MaxForeignAssets::get(),
265
				Error::<T>::TooManyForeignAssets
266
			);
267

            
268
25
			ensure!(
269
25
				T::AssetIdFilter::contains(&asset_id),
270
				Error::<T>::AssetIdFiltered
271
			);
272

            
273
25
			let symbol = core::str::from_utf8(&symbol).map_err(|_| Error::<T>::InvalidSymbol)?;
274
25
			let name = core::str::from_utf8(&name).map_err(|_| Error::<T>::InvalidTokenName)?;
275

            
276
25
			let contract_address = EvmCaller::<T>::erc20_create(asset_id, decimals, symbol, name)?;
277

            
278
			// Insert the association assetId->foreigAsset
279
			// Insert the association foreigAsset->assetId
280
25
			AssetsById::<T>::insert(&asset_id, &xcm_location);
281
25
			AssetsByLocation::<T>::insert(&xcm_location, (asset_id, AssetStatus::Active));
282
25

            
283
25
			T::OnForeignAssetCreated::on_asset_created(&xcm_location, &asset_id);
284
25

            
285
25
			Self::deposit_event(Event::ForeignAssetCreated {
286
25
				contract_address,
287
25
				asset_id,
288
25
				xcm_location,
289
25
			});
290
25
			Ok(())
291
27
		}
292

            
293
		/// Mint an asset into a specific account
294
13
		pub fn mint_into(
295
13
			asset_id: AssetId,
296
13
			beneficiary: T::AccountId,
297
13
			amount: U256,
298
13
		) -> Result<(), evm::EvmError> {
299
13
			// We perform the evm call in a storage transaction to ensure that if it fail
300
13
			// any contract storage changes are rolled back.
301
13
			frame_support::storage::with_storage_layer(|| {
302
13
				EvmCaller::<T>::erc20_mint_into(
303
13
					Self::contract_address_from_asset_id(asset_id),
304
13
					T::AccountIdToH160::convert(beneficiary),
305
13
					amount,
306
13
				)
307
13
			})
308
13
			.map_err(Into::into)
309
13
		}
310

            
311
		/// Aprrove a spender to spend a certain amount of tokens from the owner account
312
13
		pub fn approve(
313
13
			asset_id: AssetId,
314
13
			owner: T::AccountId,
315
13
			spender: T::AccountId,
316
13
			amount: U256,
317
13
		) -> Result<(), evm::EvmError> {
318
13
			// We perform the evm call in a storage transaction to ensure that if it fail
319
13
			// any contract storage changes are rolled back.
320
13
			EvmCaller::<T>::erc20_approve(
321
13
				Self::contract_address_from_asset_id(asset_id),
322
13
				T::AccountIdToH160::convert(owner),
323
13
				T::AccountIdToH160::convert(spender),
324
13
				amount,
325
13
			)
326
13
			.map_err(Into::into)
327
13
		}
328

            
329
104
		pub fn weight_of_erc20_burn() -> Weight {
330
104
			T::GasWeightMapping::gas_to_weight(evm::ERC20_BURN_FROM_GAS_LIMIT, true)
331
104
		}
332
		pub fn weight_of_erc20_mint() -> Weight {
333
			T::GasWeightMapping::gas_to_weight(evm::ERC20_MINT_INTO_GAS_LIMIT, true)
334
		}
335
20
		pub fn weight_of_erc20_transfer() -> Weight {
336
20
			T::GasWeightMapping::gas_to_weight(evm::ERC20_TRANSFER_GAS_LIMIT, true)
337
20
		}
338
		#[cfg(feature = "runtime-benchmarks")]
339
		pub fn set_asset(asset_location: Location, asset_id: AssetId) {
340
			AssetsByLocation::<T>::insert(&asset_location, (asset_id, AssetStatus::Active));
341
			AssetsById::<T>::insert(&asset_id, asset_location);
342
		}
343
	}
344

            
345
	#[pallet::call]
346
	impl<T: Config> Pallet<T> {
347
		/// Create new asset with the ForeignAssetCreator
348
		#[pallet::call_index(0)]
349
		#[pallet::weight(<T as Config>::WeightInfo::create_foreign_asset())]
350
		pub fn create_foreign_asset(
351
			origin: OriginFor<T>,
352
			asset_id: AssetId,
353
			xcm_location: Location,
354
			decimals: u8,
355
			symbol: BoundedVec<u8, ConstU32<256>>,
356
			name: BoundedVec<u8, ConstU32<256>>,
357
13
		) -> DispatchResult {
358
13
			T::ForeignAssetCreatorOrigin::ensure_origin(origin)?;
359

            
360
12
			Self::register_foreign_asset(asset_id, xcm_location, decimals, symbol, name)
361
		}
362

            
363
		/// Change the xcm type mapping for a given assetId
364
		/// We also change this if the previous units per second where pointing at the old
365
		/// assetType
366
		#[pallet::call_index(1)]
367
		#[pallet::weight(<T as Config>::WeightInfo::change_xcm_location())]
368
		pub fn change_xcm_location(
369
			origin: OriginFor<T>,
370
			asset_id: AssetId,
371
			new_xcm_location: Location,
372
4
		) -> DispatchResult {
373
4
			T::ForeignAssetModifierOrigin::ensure_origin(origin)?;
374

            
375
2
			let previous_location =
376
3
				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
377

            
378
2
			ensure!(
379
2
				!AssetsByLocation::<T>::contains_key(&new_xcm_location),
380
1
				Error::<T>::LocationAlreadyExists
381
			);
382

            
383
			// Remove previous foreign asset info
384
1
			let (_asset_id, asset_status) = AssetsByLocation::<T>::take(&previous_location)
385
1
				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
386

            
387
			// Insert new foreign asset info
388
1
			AssetsById::<T>::insert(&asset_id, &new_xcm_location);
389
1
			AssetsByLocation::<T>::insert(&new_xcm_location, (asset_id, asset_status));
390
1

            
391
1
			Self::deposit_event(Event::ForeignAssetXcmLocationChanged {
392
1
				asset_id,
393
1
				new_xcm_location,
394
1
			});
395
1
			Ok(())
396
		}
397

            
398
		/// Freeze a given foreign assetId
399
		#[pallet::call_index(2)]
400
		#[pallet::weight(<T as Config>::WeightInfo::freeze_foreign_asset())]
401
		pub fn freeze_foreign_asset(
402
			origin: OriginFor<T>,
403
			asset_id: AssetId,
404
			allow_xcm_deposit: bool,
405
3
		) -> DispatchResult {
406
3
			T::ForeignAssetFreezerOrigin::ensure_origin(origin)?;
407

            
408
3
			let xcm_location =
409
3
				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
410

            
411
3
			let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
412
3
				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
413

            
414
3
			ensure!(
415
3
				asset_status == AssetStatus::Active,
416
1
				Error::<T>::AssetAlreadyFrozen
417
			);
418

            
419
2
			EvmCaller::<T>::erc20_pause(asset_id)?;
420

            
421
2
			let new_asset_status = if allow_xcm_deposit {
422
2
				AssetStatus::FrozenXcmDepositAllowed
423
			} else {
424
				AssetStatus::FrozenXcmDepositForbidden
425
			};
426

            
427
2
			AssetsByLocation::<T>::insert(&xcm_location, (asset_id, new_asset_status));
428
2

            
429
2
			Self::deposit_event(Event::ForeignAssetFrozen {
430
2
				asset_id,
431
2
				xcm_location,
432
2
			});
433
2
			Ok(())
434
		}
435

            
436
		/// Unfreeze a given foreign assetId
437
		#[pallet::call_index(3)]
438
		#[pallet::weight(<T as Config>::WeightInfo::unfreeze_foreign_asset())]
439
3
		pub fn unfreeze_foreign_asset(origin: OriginFor<T>, asset_id: AssetId) -> DispatchResult {
440
3
			T::ForeignAssetUnfreezerOrigin::ensure_origin(origin)?;
441

            
442
3
			let xcm_location =
443
3
				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
444

            
445
3
			let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
446
3
				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
447

            
448
3
			ensure!(
449
3
				asset_status == AssetStatus::FrozenXcmDepositAllowed
450
1
					|| asset_status == AssetStatus::FrozenXcmDepositForbidden,
451
1
				Error::<T>::AssetNotFrozen
452
			);
453

            
454
2
			EvmCaller::<T>::erc20_unpause(asset_id)?;
455

            
456
2
			AssetsByLocation::<T>::insert(&xcm_location, (asset_id, AssetStatus::Active));
457
2

            
458
2
			Self::deposit_event(Event::ForeignAssetUnfrozen {
459
2
				asset_id,
460
2
				xcm_location,
461
2
			});
462
2
			Ok(())
463
		}
464
	}
465

            
466
	impl<T: Config> xcm_executor::traits::TransactAsset for Pallet<T> {
467
		// For optimization reasons, the asset we want to deposit has not really been withdrawn,
468
		// we have just traced from which account it should have been withdrawn.
469
		// So we will retrieve these information and make the transfer from the origin account.
470
		fn deposit_asset(what: &Asset, who: &Location, _context: Option<&XcmContext>) -> XcmResult {
471
			let (contract_address, amount, asset_status) =
472
				ForeignAssetsMatcher::<T>::match_asset(what)?;
473

            
474
			if let AssetStatus::FrozenXcmDepositForbidden = asset_status {
475
				return Err(MatchError::AssetNotHandled.into());
476
			}
477

            
478
			let beneficiary = T::XcmLocationToH160::convert_location(who)
479
				.ok_or(MatchError::AccountIdConversionFailed)?;
480

            
481
			// We perform the evm transfers in a storage transaction to ensure that if it fail
482
			// any contract storage changes are rolled back.
483
			frame_support::storage::with_storage_layer(|| {
484
				EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount)
485
			})?;
486

            
487
			Ok(())
488
		}
489

            
490
		fn internal_transfer_asset(
491
			asset: &Asset,
492
			from: &Location,
493
			to: &Location,
494
			_context: &XcmContext,
495
		) -> Result<AssetsInHolding, XcmError> {
496
			let (contract_address, amount, asset_status) =
497
				ForeignAssetsMatcher::<T>::match_asset(asset)?;
498

            
499
			if let AssetStatus::FrozenXcmDepositForbidden | AssetStatus::FrozenXcmDepositAllowed =
500
				asset_status
501
			{
502
				return Err(MatchError::AssetNotHandled.into());
503
			}
504

            
505
			let from = T::XcmLocationToH160::convert_location(from)
506
				.ok_or(MatchError::AccountIdConversionFailed)?;
507

            
508
			let to = T::XcmLocationToH160::convert_location(to)
509
				.ok_or(MatchError::AccountIdConversionFailed)?;
510

            
511
			// We perform the evm transfers in a storage transaction to ensure that if it fail
512
			// any contract storage changes are rolled back.
513
			frame_support::storage::with_storage_layer(|| {
514
				EvmCaller::<T>::erc20_transfer(contract_address, from, to, amount)
515
			})?;
516

            
517
			Ok(asset.clone().into())
518
		}
519

            
520
		// Since we don't control the erc20 contract that manages the asset we want to withdraw,
521
		// we can't really withdraw this asset, we can only transfer it to another account.
522
		// It would be possible to transfer the asset to a dedicated account that would reflect
523
		// the content of the xcm holding, but this would imply to perform two evm calls instead of
524
		// one (1 to withdraw the asset and a second one to deposit it).
525
		// In order to perform only one evm call, we just trace the origin of the asset,
526
		// and then the transfer will only really be performed in the deposit instruction.
527
52
		fn withdraw_asset(
528
52
			what: &Asset,
529
52
			who: &Location,
530
52
			_context: Option<&XcmContext>,
531
52
		) -> Result<AssetsInHolding, XcmError> {
532
18
			let (contract_address, amount, asset_status) =
533
52
				ForeignAssetsMatcher::<T>::match_asset(what)?;
534
18
			let who = T::XcmLocationToH160::convert_location(who)
535
18
				.ok_or(MatchError::AccountIdConversionFailed)?;
536

            
537
18
			if let AssetStatus::FrozenXcmDepositForbidden | AssetStatus::FrozenXcmDepositAllowed =
538
18
				asset_status
539
			{
540
				return Err(MatchError::AssetNotHandled.into());
541
18
			}
542
18

            
543
18
			// We perform the evm transfers in a storage transaction to ensure that if it fail
544
18
			// any contract storage changes are rolled back.
545
18
			frame_support::storage::with_storage_layer(|| {
546
18
				EvmCaller::<T>::erc20_burn_from(contract_address, who, amount)
547
18
			})?;
548

            
549
18
			Ok(what.clone().into())
550
52
		}
551
	}
552

            
553
	impl<T: Config> sp_runtime::traits::MaybeEquivalence<Location, AssetId> for Pallet<T> {
554
		fn convert(location: &Location) -> Option<AssetId> {
555
			AssetsByLocation::<T>::get(location).map(|(asset_id, _)| asset_id)
556
		}
557
3
		fn convert_back(asset_id: &AssetId) -> Option<Location> {
558
3
			AssetsById::<T>::get(asset_id)
559
3
		}
560
	}
561
}