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"))]
41
pub mod benchmarks;
42
#[cfg(feature = "runtime-benchmarks")]
43
pub use benchmarks::*;
44
#[cfg(test)]
45
pub mod mock;
46
#[cfg(test)]
47
pub mod tests;
48
pub mod weights;
49

            
50
mod evm;
51

            
52
pub use pallet::*;
53
pub use weights::WeightInfo;
54

            
55
use self::evm::EvmCaller;
56
use ethereum_types::{H160, U256};
57
use frame_support::pallet_prelude::*;
58
use frame_support::traits::Contains;
59
use frame_support::{pallet, Deserialize, Serialize};
60
use frame_system::pallet_prelude::*;
61
use sp_std::{vec, vec::Vec};
62
use xcm::latest::{
63
	Asset, AssetId as XcmAssetId, Error as XcmError, Fungibility, Location, Result as XcmResult,
64
	XcmContext,
65
};
66
use xcm::prelude::Parachain;
67
use xcm_executor::traits::ConvertLocation;
68
use xcm_executor::traits::Error as MatchError;
69

            
70
const FOREIGN_ASSETS_PREFIX: [u8; 4] = [0xff, 0xff, 0xff, 0xff];
71

            
72
/// Trait for the OnForeignAssetRegistered hook
73
pub trait ForeignAssetCreatedHook<ForeignAsset> {
74
	fn on_asset_created(foreign_asset: &ForeignAsset, asset_id: &AssetId);
75
}
76

            
77
impl<ForeignAsset> ForeignAssetCreatedHook<ForeignAsset> for () {
78
125
	fn on_asset_created(_foreign_asset: &ForeignAsset, _asset_id: &AssetId) {}
79
}
80

            
81
/// Ensure origin location is a sibling
82
4
fn convert_location<T>(location: &Location) -> Result<T::AccountId, DispatchError>
83
4
where
84
4
	T: Config,
85
{
86
4
	match location.unpack() {
87
4
		(1, [Parachain(_)]) => T::ConvertLocation::convert_location(location)
88
4
			.ok_or(Error::<T>::CannotConvertLocationToAccount.into()),
89
		_ => Err(DispatchError::BadOrigin.into()),
90
	}
91
4
}
92
#[derive(Decode, Encode, Debug, PartialEq, TypeInfo, Clone)]
93
pub enum OriginType {
94
	XCM(Location),
95
	Governance,
96
}
97

            
98
/// Used to convert the success of an EnsureOrigin into `OriginType::Governance`
99
pub struct MapSuccessToGovernance<Original>(PhantomData<Original>);
100
impl<O, Original: EnsureOrigin<O, Success = ()>> EnsureOrigin<O>
101
	for MapSuccessToGovernance<Original>
102
{
103
	type Success = OriginType;
104
51
	fn try_origin(o: O) -> Result<OriginType, O> {
105
51
		Original::try_origin(o)?;
106
39
		Ok(OriginType::Governance)
107
51
	}
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`
115
pub struct MapSuccessToXcm<Original>(PhantomData<Original>);
116
impl<O, Original: EnsureOrigin<O, Success = Location>> EnsureOrigin<O>
117
	for MapSuccessToXcm<Original>
118
{
119
	type Success = OriginType;
120
42
	fn try_origin(o: O) -> Result<OriginType, O> {
121
42
		Original::try_origin(o).map(OriginType::XCM)
122
42
	}
123
	#[cfg(feature = "runtime-benchmarks")]
124
	fn try_successful_origin() -> Result<O, ()> {
125
		Original::try_successful_origin()
126
	}
127
}
128

            
129
pub(crate) struct ForeignAssetsMatcher<T>(core::marker::PhantomData<T>);
130

            
131
impl<T: crate::Config> ForeignAssetsMatcher<T> {
132
222
	fn match_asset(asset: &Asset) -> Result<(H160, U256, AssetStatus), MatchError> {
133
222
		let (amount, location) = match (&asset.fun, &asset.id) {
134
222
			(Fungibility::Fungible(ref amount), XcmAssetId(ref location)) => (amount, location),
135
			_ => return Err(MatchError::AssetNotHandled),
136
		};
137

            
138
222
		if let Some((asset_id, asset_status)) = AssetsByLocation::<T>::get(&location) {
139
222
			Ok((
140
222
				Pallet::<T>::contract_address_from_asset_id(asset_id),
141
222
				U256::from(*amount),
142
222
				asset_status,
143
222
			))
144
		} else {
145
			Err(MatchError::AssetNotHandled)
146
		}
147
222
	}
148
}
149

            
150
#[derive(Decode, Debug, Encode, PartialEq, TypeInfo)]
151
pub 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)]
161
pub 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]
170
pub 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
6
		fn default() -> Self {
343
6
			Self {
344
6
				assets: vec![],
345
6
				_phantom: Default::default(),
346
6
			}
347
6
		}
348
	}
349

            
350
	#[pallet::genesis_build]
351
	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
352
6
		fn build(&self) {
353
6
			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
6
		}
364
	}
365

            
366
	impl<T: Config> Pallet<T> {
367
		/// The account ID of this pallet
368
		#[inline]
369
698
		pub fn account_id() -> H160 {
370
698
			let account_id: T::AccountId = PALLET_ID.into_account_truncating();
371
698
			T::AccountIdToH160::convert(account_id)
372
698
		}
373

            
374
		/// Compute asset contract address from asset id
375
		#[inline]
376
571
		pub fn contract_address_from_asset_id(asset_id: AssetId) -> H160 {
377
571
			let mut buffer = [0u8; 20];
378
571
			buffer[..4].copy_from_slice(&FOREIGN_ASSETS_PREFIX);
379
571
			buffer[4..].copy_from_slice(&asset_id.to_be_bytes());
380
571
			H160(buffer)
381
571
		}
382

            
383
		/// This method only exists for migration purposes and will be deleted once the
384
		/// foreign assets migration is finished.
385
103
		pub fn register_foreign_asset(
386
103
			asset_id: AssetId,
387
103
			xcm_location: Location,
388
103
			decimals: u8,
389
103
			symbol: BoundedVec<u8, ConstU32<256>>,
390
103
			name: BoundedVec<u8, ConstU32<256>>,
391
103
		) -> DispatchResult {
392
103
			Self::do_create_asset(asset_id, xcm_location, decimals, symbol, name, None)
393
103
		}
394

            
395
		/// Mint an asset into a specific account
396
16
		pub fn mint_into(
397
16
			asset_id: AssetId,
398
16
			beneficiary: T::AccountId,
399
16
			amount: U256,
400
16
		) -> 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
16
			frame_support::storage::with_storage_layer(|| {
404
16
				EvmCaller::<T>::erc20_mint_into(
405
16
					Self::contract_address_from_asset_id(asset_id),
406
16
					T::AccountIdToH160::convert(beneficiary),
407
16
					amount,
408
				)
409
16
			})
410
16
			.map_err(Into::into)
411
16
		}
412

            
413
		/// Transfer an asset from an account to another one
414
7
		pub fn transfer(
415
7
			asset_id: AssetId,
416
7
			from: T::AccountId,
417
7
			to: T::AccountId,
418
7
			amount: U256,
419
7
		) -> Result<(), evm::EvmError> {
420
7
			frame_support::storage::with_storage_layer(|| {
421
7
				EvmCaller::<T>::erc20_transfer(
422
7
					Self::contract_address_from_asset_id(asset_id),
423
7
					T::AccountIdToH160::convert(from),
424
7
					T::AccountIdToH160::convert(to),
425
7
					amount,
426
				)
427
7
			})
428
7
			.map_err(Into::into)
429
7
		}
430

            
431
181
		pub fn balance(asset_id: AssetId, who: T::AccountId) -> Result<U256, evm::EvmError> {
432
181
			EvmCaller::<T>::erc20_balance_of(asset_id, T::AccountIdToH160::convert(who))
433
181
				.map_err(Into::into)
434
181
		}
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
32
		) -> DispatchResult {
495
32
			let origin_type = T::ForeignAssetCreatorOrigin::ensure_origin(origin.clone())?;
496

            
497
31
			Self::ensure_origin_can_modify_location(origin_type.clone(), &asset_xcm_location)?;
498
31
			let deposit_account = Self::get_deposit_account(origin_type)?;
499

            
500
31
			Self::do_create_asset(
501
31
				asset_id,
502
31
				asset_xcm_location,
503
31
				decimals,
504
31
				symbol,
505
31
				name,
506
31
				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
6
		) -> DispatchResult {
520
6
			let origin_type = T::ForeignAssetModifierOrigin::ensure_origin(origin.clone())?;
521

            
522
5
			Self::ensure_origin_can_modify_location(origin_type.clone(), &new_xcm_location)?;
523

            
524
4
			let previous_location =
525
5
				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
526

            
527
4
			Self::ensure_origin_can_modify_location(origin_type, &previous_location)?;
528

            
529
3
			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
11
		) -> DispatchResult {
540
11
			let origin_type = T::ForeignAssetFreezerOrigin::ensure_origin(origin.clone())?;
541

            
542
11
			let xcm_location =
543
11
				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
544

            
545
11
			Self::ensure_origin_can_modify_location(origin_type, &xcm_location)?;
546

            
547
9
			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
8
		pub fn unfreeze_foreign_asset(origin: OriginFor<T>, asset_id: AssetId) -> DispatchResult {
554
8
			let origin_type = T::ForeignAssetUnfreezerOrigin::ensure_origin(origin.clone())?;
555

            
556
8
			let xcm_location =
557
8
				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
558

            
559
8
			Self::ensure_origin_can_modify_location(origin_type, &xcm_location)?;
560

            
561
8
			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
59
		fn ensure_origin_can_modify_location(
568
59
			origin_type: OriginType,
569
59
			location: &Location,
570
59
		) -> DispatchResult {
571
59
			match origin_type {
572
17
				OriginType::XCM(origin_location) => {
573
17
					ensure!(
574
17
						location.starts_with(&origin_location),
575
3
						Error::<T>::LocationOutsideOfOrigin,
576
					);
577
				}
578
42
				OriginType::Governance => {
579
42
					// nothing to check Governance can change any asset
580
42
				}
581
			};
582
56
			Ok(())
583
59
		}
584

            
585
31
		fn get_deposit_account(
586
31
			origin_type: OriginType,
587
31
		) -> Result<Option<T::AccountId>, DispatchError> {
588
31
			match origin_type {
589
4
				OriginType::XCM(origin_location) => {
590
4
					let deposit_account = convert_location::<T>(&origin_location)?;
591
4
					Ok(Some(deposit_account))
592
				}
593
27
				OriginType::Governance => Ok(None),
594
			}
595
31
		}
596

            
597
134
		pub fn do_create_asset(
598
134
			asset_id: AssetId,
599
134
			asset_xcm_location: Location,
600
134
			decimals: u8,
601
134
			symbol: BoundedVec<u8, ConstU32<256>>,
602
134
			name: BoundedVec<u8, ConstU32<256>>,
603
134
			deposit_account: Option<T::AccountId>,
604
134
		) -> DispatchResult {
605
134
			ensure!(
606
134
				!AssetsById::<T>::contains_key(&asset_id),
607
1
				Error::<T>::AssetAlreadyExists
608
			);
609

            
610
133
			ensure!(
611
133
				!AssetsByLocation::<T>::contains_key(&asset_xcm_location),
612
1
				Error::<T>::LocationAlreadyExists
613
			);
614

            
615
132
			ensure!(
616
132
				AssetsById::<T>::count() < T::MaxForeignAssets::get(),
617
				Error::<T>::TooManyForeignAssets
618
			);
619

            
620
132
			ensure!(
621
132
				T::AssetIdFilter::contains(&asset_id),
622
				Error::<T>::AssetIdFiltered
623
			);
624

            
625
132
			let symbol = core::str::from_utf8(&symbol).map_err(|_| Error::<T>::InvalidSymbol)?;
626
132
			let name = core::str::from_utf8(&name).map_err(|_| Error::<T>::InvalidTokenName)?;
627
132
			let contract_address = EvmCaller::<T>::erc20_create(asset_id, decimals, symbol, name)?;
628

            
629
132
			let deposit = if let Some(deposit_account) = deposit_account {
630
4
				let deposit = T::ForeignAssetCreationDeposit::get();
631

            
632
				// Reserve _deposit_ amount of funds from the caller
633
4
				<T as Config>::Currency::reserve(&deposit_account, deposit)?;
634

            
635
				// Insert the amount that is reserved from the user
636
4
				AssetsCreationDetails::<T>::insert(
637
4
					&asset_id,
638
4
					AssetDepositDetails {
639
4
						deposit_account,
640
4
						deposit,
641
4
					},
642
				);
643

            
644
4
				Some(deposit)
645
			} else {
646
128
				None
647
			};
648

            
649
			// Insert the association assetId->foreigAsset
650
			// Insert the association foreigAsset->assetId
651
132
			AssetsById::<T>::insert(&asset_id, &asset_xcm_location);
652
132
			AssetsByLocation::<T>::insert(&asset_xcm_location, (asset_id, AssetStatus::Active));
653

            
654
132
			T::OnForeignAssetCreated::on_asset_created(&asset_xcm_location, &asset_id);
655

            
656
132
			Self::deposit_event(Event::ForeignAssetCreated {
657
132
				contract_address,
658
132
				asset_id,
659
132
				xcm_location: asset_xcm_location,
660
132
				deposit,
661
132
			});
662
132
			Ok(())
663
134
		}
664

            
665
3
		pub fn do_change_xcm_location(
666
3
			asset_id: AssetId,
667
3
			previous_xcm_location: Location,
668
3
			new_xcm_location: Location,
669
3
		) -> DispatchResult {
670
3
			ensure!(
671
3
				!AssetsByLocation::<T>::contains_key(&new_xcm_location),
672
1
				Error::<T>::LocationAlreadyExists
673
			);
674

            
675
			// Remove previous foreign asset info
676
2
			let (_asset_id, asset_status) = AssetsByLocation::<T>::take(&previous_xcm_location)
677
2
				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
678

            
679
			// Insert new foreign asset info
680
2
			AssetsById::<T>::insert(&asset_id, &new_xcm_location);
681
2
			AssetsByLocation::<T>::insert(&new_xcm_location, (asset_id, asset_status));
682

            
683
2
			Self::deposit_event(Event::ForeignAssetXcmLocationChanged {
684
2
				asset_id,
685
2
				new_xcm_location,
686
2
				previous_xcm_location,
687
2
			});
688
2
			Ok(())
689
3
		}
690

            
691
9
		pub fn do_freeze_asset(
692
9
			asset_id: AssetId,
693
9
			xcm_location: Location,
694
9
			allow_xcm_deposit: bool,
695
9
		) -> DispatchResult {
696
9
			let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
697
9
				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
698

            
699
9
			ensure!(
700
9
				asset_status == AssetStatus::Active,
701
2
				Error::<T>::AssetAlreadyFrozen
702
			);
703

            
704
7
			EvmCaller::<T>::erc20_pause(asset_id)?;
705

            
706
7
			let new_asset_status = if allow_xcm_deposit {
707
7
				AssetStatus::FrozenXcmDepositAllowed
708
			} else {
709
				AssetStatus::FrozenXcmDepositForbidden
710
			};
711

            
712
7
			AssetsByLocation::<T>::insert(&xcm_location, (asset_id, new_asset_status));
713

            
714
7
			Self::deposit_event(Event::ForeignAssetFrozen {
715
7
				asset_id,
716
7
				xcm_location,
717
7
			});
718
7
			Ok(())
719
9
		}
720

            
721
8
		pub fn do_unfreeze_asset(asset_id: AssetId, xcm_location: Location) -> DispatchResult {
722
8
			let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
723
8
				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
724

            
725
8
			ensure!(
726
8
				asset_status == AssetStatus::FrozenXcmDepositAllowed
727
2
					|| asset_status == AssetStatus::FrozenXcmDepositForbidden,
728
2
				Error::<T>::AssetNotFrozen
729
			);
730

            
731
6
			EvmCaller::<T>::erc20_unpause(asset_id)?;
732

            
733
6
			AssetsByLocation::<T>::insert(&xcm_location, (asset_id, AssetStatus::Active));
734

            
735
6
			Self::deposit_event(Event::ForeignAssetUnfrozen {
736
6
				asset_id,
737
6
				xcm_location,
738
6
			});
739
6
			Ok(())
740
8
		}
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
125
		fn deposit_asset(what: &Asset, who: &Location, _context: Option<&XcmContext>) -> XcmResult {
748
125
			let (contract_address, amount, asset_status) =
749
125
				ForeignAssetsMatcher::<T>::match_asset(what)?;
750

            
751
125
			if let AssetStatus::FrozenXcmDepositForbidden = asset_status {
752
				return Err(MatchError::AssetNotHandled.into());
753
125
			}
754

            
755
125
			let beneficiary = T::XcmLocationToH160::convert_location(who)
756
125
				.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
125
			frame_support::storage::with_storage_layer(|| {
761
125
				EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount)
762
125
			})?;
763

            
764
125
			Ok(())
765
125
		}
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
97
		fn withdraw_asset(
805
97
			what: &Asset,
806
97
			who: &Location,
807
97
			_context: Option<&XcmContext>,
808
97
		) -> Result<AssetsInHolding, XcmError> {
809
97
			let (contract_address, amount, asset_status) =
810
97
				ForeignAssetsMatcher::<T>::match_asset(what)?;
811
97
			let who = T::XcmLocationToH160::convert_location(who)
812
97
				.ok_or(MatchError::AccountIdConversionFailed)?;
813

            
814
			if let AssetStatus::FrozenXcmDepositForbidden | AssetStatus::FrozenXcmDepositAllowed =
815
97
				asset_status
816
			{
817
				return Err(MatchError::AssetNotHandled.into());
818
97
			}
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
97
			frame_support::storage::with_storage_layer(|| {
823
97
				EvmCaller::<T>::erc20_burn_from(contract_address, who, amount)
824
97
			})?;
825

            
826
97
			Ok(what.clone().into())
827
97
		}
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
91
		fn convert_back(asset_id: &AssetId) -> Option<Location> {
846
91
			AssetsById::<T>::get(asset_id)
847
91
		}
848
	}
849
}