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
129
	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
4
{
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
55
	fn try_origin(o: O) -> Result<OriginType, O> {
105
55
		Original::try_origin(o)?;
106
43
		Ok(OriginType::Governance)
107
55
	}
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
46
	fn try_origin(o: O) -> Result<OriginType, O> {
121
46
		Original::try_origin(o).map(OriginType::XCM)
122
46
	}
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
224
	fn match_asset(asset: &Asset) -> Result<(H160, U256, AssetStatus), MatchError> {
133
224
		let (amount, location) = match (&asset.fun, &asset.id) {
134
224
			(Fungibility::Fungible(ref amount), XcmAssetId(ref location)) => (amount, location),
135
			_ => return Err(MatchError::AssetNotHandled),
136
		};
137

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

            
150
171
#[derive(Decode, Debug, Encode, PartialEq, TypeInfo)]
151
pub enum AssetStatus {
152
254
	/// All operations are enabled
153
	Active,
154
14
	/// 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
980
#[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
130
	#[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: frame_system::Config + pallet_evm::Config + scale_info::TypeInfo {
188
		// Convert AccountId to H160
189
		type AccountIdToH160: Convert<Self::AccountId, H160>;
190

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

            
194
		/// EVM runner
195
		type EvmRunner: Runner<Self>;
196

            
197
		type ConvertLocation: ConvertLocation<Self::AccountId>;
198

            
199
		/// Origin that is allowed to create a new foreign assets
200
		type ForeignAssetCreatorOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = OriginType>;
201

            
202
		/// Origin that is allowed to freeze all tokens of a foreign asset
203
		type ForeignAssetFreezerOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = OriginType>;
204

            
205
		/// Origin that is allowed to modify asset information for foreign assets
206
		type ForeignAssetModifierOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = OriginType>;
207

            
208
		/// Origin that is allowed to unfreeze all tokens of a foreign asset that was previously
209
		/// frozen
210
		type ForeignAssetUnfreezerOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = OriginType>;
211

            
212
		/// Hook to be called when new foreign asset is registered.
213
		type OnForeignAssetCreated: ForeignAssetCreatedHook<Location>;
214

            
215
		/// Maximum numbers of different foreign assets
216
		type MaxForeignAssets: Get<u32>;
217

            
218
		/// The overarching event type.
219
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
220

            
221
		/// Weight information for extrinsics in this pallet.
222
		type WeightInfo: WeightInfo;
223

            
224
		// Convert XCM Location to H160
225
		type XcmLocationToH160: ConvertLocation<H160>;
226

            
227
		/// Amount of tokens required to lock for creating a new foreign asset
228
		type ForeignAssetCreationDeposit: Get<BalanceOf<Self>>;
229

            
230
		/// The balance type for locking funds
231
		type Balance: Member
232
			+ Parameter
233
			+ AtLeast32BitUnsigned
234
			+ Default
235
			+ Copy
236
			+ MaybeSerializeDeserialize
237
			+ MaxEncodedLen
238
			+ TypeInfo;
239

            
240
		/// The currency type for locking funds
241
		type Currency: ReservableCurrency<Self::AccountId>;
242
	}
243

            
244
	type BalanceOf<T> =
245
		<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
246

            
247
	pub type AssetBalance = U256;
248
	pub type AssetId = u128;
249

            
250
	/// An error that can occur while executing the mapping pallet's logic.
251
20
	#[pallet::error]
252
	pub enum Error<T> {
253
		AssetAlreadyExists,
254
		AssetAlreadyFrozen,
255
		AssetDoesNotExist,
256
		AssetIdFiltered,
257
		AssetNotFrozen,
258
		CorruptedStorageOrphanLocation,
259
		Erc20ContractCreationFail,
260
		EvmCallPauseFail,
261
		EvmCallUnpauseFail,
262
		EvmCallMintIntoFail,
263
		EvmCallTransferFail,
264
		EvmInternalError,
265
		/// Account has insufficient balance for locking
266
		InsufficientBalance,
267
		CannotConvertLocationToAccount,
268
		LocationOutsideOfOrigin,
269
		AssetNotInSiblingPara,
270
		InvalidSymbol,
271
		InvalidTokenName,
272
		LocationAlreadyExists,
273
		TooManyForeignAssets,
274
	}
275

            
276
20
	#[pallet::event]
277
151
	#[pallet::generate_deposit(pub(crate) fn deposit_event)]
278
	pub enum Event<T: Config> {
279
		/// New asset with the asset manager is registered
280
		ForeignAssetCreated {
281
			contract_address: H160,
282
			asset_id: AssetId,
283
			xcm_location: Location,
284
			deposit: Option<BalanceOf<T>>,
285
		},
286
		/// Changed the xcm type mapping for a given asset id
287
		ForeignAssetXcmLocationChanged {
288
			asset_id: AssetId,
289
			previous_xcm_location: Location,
290
			new_xcm_location: Location,
291
		},
292
		// Freezes all tokens of a given asset id
293
		ForeignAssetFrozen {
294
			asset_id: AssetId,
295
			xcm_location: Location,
296
		},
297
		// Thawing a previously frozen asset
298
		ForeignAssetUnfrozen {
299
			asset_id: AssetId,
300
			xcm_location: Location,
301
		},
302
		/// Tokens have been locked for asset creation
303
		TokensLocked(T::AccountId, AssetId, AssetBalance),
304
	}
305

            
306
	/// Mapping from an asset id to a Foreign asset type.
307
	/// This is mostly used when receiving transaction specifying an asset directly,
308
	/// like transferring an asset from this chain to another.
309
1399
	#[pallet::storage]
310
	#[pallet::getter(fn assets_by_id)]
311
	pub type AssetsById<T: Config> =
312
		CountedStorageMap<_, Blake2_128Concat, AssetId, Location, OptionQuery>;
313

            
314
	/// Reverse mapping of AssetsById. Mapping from a foreign asset to an asset id.
315
	/// This is mostly used when receiving a multilocation XCM message to retrieve
316
	/// the corresponding asset in which tokens should me minted.
317
606
	#[pallet::storage]
318
	#[pallet::getter(fn assets_by_location)]
319
	pub type AssetsByLocation<T: Config> =
320
		StorageMap<_, Blake2_128Concat, Location, (AssetId, AssetStatus)>;
321

            
322
	/// Mapping from an asset id to its creation details
323
24
	#[pallet::storage]
324
	#[pallet::getter(fn assets_creation_details)]
325
	pub type AssetsCreationDetails<T: Config> =
326
		StorageMap<_, Blake2_128Concat, AssetId, AssetDepositDetails<T>>;
327

            
328
40
	#[derive(Clone, Decode, Encode, Eq, PartialEq, Debug, TypeInfo, MaxEncodedLen)]
329
	pub struct AssetDepositDetails<T: Config> {
330
		pub deposit_account: T::AccountId,
331
		pub deposit: BalanceOf<T>,
332
	}
333

            
334
	#[pallet::genesis_config]
335
	pub struct GenesisConfig<T: Config> {
336
		pub assets: Vec<EvmForeignAssetInfo>,
337
		pub _phantom: PhantomData<T>,
338
	}
339

            
340
	impl<T: Config> Default for GenesisConfig<T> {
341
6
		fn default() -> Self {
342
6
			Self {
343
6
				assets: vec![],
344
6
				_phantom: Default::default(),
345
6
			}
346
6
		}
347
	}
348

            
349
	#[pallet::genesis_build]
350
	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
351
6
		fn build(&self) {
352
6
			for asset in self.assets.clone() {
353
				Pallet::<T>::register_foreign_asset(
354
					asset.asset_id,
355
					asset.xcm_location,
356
					asset.decimals,
357
					asset.symbol,
358
					asset.name,
359
				)
360
				.expect("couldn't register asset");
361
			}
362
6
		}
363
	}
364

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

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

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

            
394
		/// Mint an asset into a specific account
395
20
		pub fn mint_into(
396
20
			asset_id: AssetId,
397
20
			beneficiary: T::AccountId,
398
20
			amount: U256,
399
20
		) -> Result<(), evm::EvmError> {
400
20
			// We perform the evm call in a storage transaction to ensure that if it fail
401
20
			// any contract storage changes are rolled back.
402
20
			frame_support::storage::with_storage_layer(|| {
403
20
				EvmCaller::<T>::erc20_mint_into(
404
20
					Self::contract_address_from_asset_id(asset_id),
405
20
					T::AccountIdToH160::convert(beneficiary),
406
20
					amount,
407
20
				)
408
20
			})
409
20
			.map_err(Into::into)
410
20
		}
411

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

            
430
189
		pub fn balance(asset_id: AssetId, who: T::AccountId) -> Result<U256, evm::EvmError> {
431
189
			EvmCaller::<T>::erc20_balance_of(asset_id, T::AccountIdToH160::convert(who))
432
189
				.map_err(Into::into)
433
189
		}
434

            
435
		/// Approve a spender to spend a certain amount of tokens from the owner account
436
		pub fn approve(
437
			asset_id: AssetId,
438
			owner: T::AccountId,
439
			spender: T::AccountId,
440
			amount: U256,
441
		) -> Result<(), evm::EvmError> {
442
			// We perform the evm call in a storage transaction to ensure that if it fail
443
			// any contract storage changes are rolled back.
444
			frame_support::storage::with_storage_layer(|| {
445
				EvmCaller::<T>::erc20_approve(
446
					Self::contract_address_from_asset_id(asset_id),
447
					T::AccountIdToH160::convert(owner),
448
					T::AccountIdToH160::convert(spender),
449
					amount,
450
				)
451
			})
452
			.map_err(Into::into)
453
		}
454

            
455
48
		pub fn weight_of_erc20_burn() -> Weight {
456
48
			T::GasWeightMapping::gas_to_weight(evm::ERC20_BURN_FROM_GAS_LIMIT, true)
457
48
		}
458
2
		pub fn weight_of_erc20_mint() -> Weight {
459
2
			T::GasWeightMapping::gas_to_weight(evm::ERC20_MINT_INTO_GAS_LIMIT, true)
460
2
		}
461
22
		pub fn weight_of_erc20_transfer() -> Weight {
462
22
			T::GasWeightMapping::gas_to_weight(evm::ERC20_TRANSFER_GAS_LIMIT, true)
463
22
		}
464
		#[cfg(feature = "runtime-benchmarks")]
465
		pub fn set_asset(asset_location: Location, asset_id: AssetId) {
466
			AssetsByLocation::<T>::insert(&asset_location, (asset_id, AssetStatus::Active));
467
			AssetsById::<T>::insert(&asset_id, asset_location);
468
		}
469
	}
470

            
471
20
	#[pallet::call]
472
	impl<T: Config> Pallet<T> {
473
		/// Create new asset with the ForeignAssetCreator
474
		#[pallet::call_index(0)]
475
		#[pallet::weight(<T as Config>::WeightInfo::create_foreign_asset())]
476
		pub fn create_foreign_asset(
477
			origin: OriginFor<T>,
478
			asset_id: AssetId,
479
			asset_xcm_location: Location,
480
			decimals: u8,
481
			symbol: BoundedVec<u8, ConstU32<256>>,
482
			name: BoundedVec<u8, ConstU32<256>>,
483
36
		) -> DispatchResult {
484
36
			let origin_type = T::ForeignAssetCreatorOrigin::ensure_origin(origin.clone())?;
485

            
486
35
			Self::ensure_origin_can_modify_location(origin_type.clone(), &asset_xcm_location)?;
487
35
			let deposit_account = Self::get_deposit_account(origin_type)?;
488

            
489
35
			Self::do_create_asset(
490
35
				asset_id,
491
35
				asset_xcm_location,
492
35
				decimals,
493
35
				symbol,
494
35
				name,
495
35
				deposit_account,
496
35
			)
497
		}
498

            
499
		/// Change the xcm type mapping for a given assetId
500
		/// We also change this if the previous units per second where pointing at the old
501
		/// assetType
502
		#[pallet::call_index(1)]
503
		#[pallet::weight(<T as Config>::WeightInfo::change_xcm_location())]
504
		pub fn change_xcm_location(
505
			origin: OriginFor<T>,
506
			asset_id: AssetId,
507
			new_xcm_location: Location,
508
6
		) -> DispatchResult {
509
6
			let origin_type = T::ForeignAssetModifierOrigin::ensure_origin(origin.clone())?;
510

            
511
5
			Self::ensure_origin_can_modify_location(origin_type.clone(), &new_xcm_location)?;
512

            
513
4
			let previous_location =
514
5
				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
515

            
516
4
			Self::ensure_origin_can_modify_location(origin_type, &previous_location)?;
517

            
518
3
			Self::do_change_xcm_location(asset_id, previous_location, new_xcm_location)
519
		}
520

            
521
		/// Freeze a given foreign assetId
522
		#[pallet::call_index(2)]
523
		#[pallet::weight(<T as Config>::WeightInfo::freeze_foreign_asset())]
524
		pub fn freeze_foreign_asset(
525
			origin: OriginFor<T>,
526
			asset_id: AssetId,
527
			allow_xcm_deposit: bool,
528
11
		) -> DispatchResult {
529
11
			let origin_type = T::ForeignAssetFreezerOrigin::ensure_origin(origin.clone())?;
530

            
531
11
			let xcm_location =
532
11
				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
533

            
534
11
			Self::ensure_origin_can_modify_location(origin_type, &xcm_location)?;
535

            
536
9
			Self::do_freeze_asset(asset_id, xcm_location, allow_xcm_deposit)
537
		}
538

            
539
		/// Unfreeze a given foreign assetId
540
		#[pallet::call_index(3)]
541
		#[pallet::weight(<T as Config>::WeightInfo::unfreeze_foreign_asset())]
542
8
		pub fn unfreeze_foreign_asset(origin: OriginFor<T>, asset_id: AssetId) -> DispatchResult {
543
8
			let origin_type = T::ForeignAssetUnfreezerOrigin::ensure_origin(origin.clone())?;
544

            
545
8
			let xcm_location =
546
8
				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
547

            
548
8
			Self::ensure_origin_can_modify_location(origin_type, &xcm_location)?;
549

            
550
8
			Self::do_unfreeze_asset(asset_id, xcm_location)
551
		}
552
	}
553

            
554
	impl<T: Config> Pallet<T> {
555
		/// Ensure that the caller origin can modify the location,
556
63
		fn ensure_origin_can_modify_location(
557
63
			origin_type: OriginType,
558
63
			location: &Location,
559
63
		) -> DispatchResult {
560
63
			match origin_type {
561
17
				OriginType::XCM(origin_location) => {
562
17
					ensure!(
563
17
						location.starts_with(&origin_location),
564
3
						Error::<T>::LocationOutsideOfOrigin,
565
					);
566
				}
567
46
				OriginType::Governance => {
568
46
					// nothing to check Governance can change any asset
569
46
				}
570
			};
571
60
			Ok(())
572
63
		}
573

            
574
35
		fn get_deposit_account(
575
35
			origin_type: OriginType,
576
35
		) -> Result<Option<T::AccountId>, DispatchError> {
577
35
			match origin_type {
578
4
				OriginType::XCM(origin_location) => {
579
4
					let deposit_account = convert_location::<T>(&origin_location)?;
580
4
					Ok(Some(deposit_account))
581
				}
582
31
				OriginType::Governance => Ok(None),
583
			}
584
35
		}
585

            
586
138
		pub fn do_create_asset(
587
138
			asset_id: AssetId,
588
138
			asset_xcm_location: Location,
589
138
			decimals: u8,
590
138
			symbol: BoundedVec<u8, ConstU32<256>>,
591
138
			name: BoundedVec<u8, ConstU32<256>>,
592
138
			deposit_account: Option<T::AccountId>,
593
138
		) -> DispatchResult {
594
138
			ensure!(
595
138
				!AssetsById::<T>::contains_key(&asset_id),
596
1
				Error::<T>::AssetAlreadyExists
597
			);
598

            
599
137
			ensure!(
600
137
				!AssetsByLocation::<T>::contains_key(&asset_xcm_location),
601
1
				Error::<T>::LocationAlreadyExists
602
			);
603

            
604
136
			ensure!(
605
136
				AssetsById::<T>::count() < T::MaxForeignAssets::get(),
606
				Error::<T>::TooManyForeignAssets
607
			);
608

            
609
136
			ensure!(
610
136
				T::AssetIdFilter::contains(&asset_id),
611
				Error::<T>::AssetIdFiltered
612
			);
613

            
614
136
			let symbol = core::str::from_utf8(&symbol).map_err(|_| Error::<T>::InvalidSymbol)?;
615
136
			let name = core::str::from_utf8(&name).map_err(|_| Error::<T>::InvalidTokenName)?;
616
136
			let contract_address = EvmCaller::<T>::erc20_create(asset_id, decimals, symbol, name)?;
617

            
618
136
			let deposit = if let Some(deposit_account) = deposit_account {
619
4
				let deposit = T::ForeignAssetCreationDeposit::get();
620
4

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

            
624
				// Insert the amount that is reserved from the user
625
4
				AssetsCreationDetails::<T>::insert(
626
4
					&asset_id,
627
4
					AssetDepositDetails {
628
4
						deposit_account,
629
4
						deposit,
630
4
					},
631
4
				);
632
4

            
633
4
				Some(deposit)
634
			} else {
635
132
				None
636
			};
637

            
638
			// Insert the association assetId->foreigAsset
639
			// Insert the association foreigAsset->assetId
640
136
			AssetsById::<T>::insert(&asset_id, &asset_xcm_location);
641
136
			AssetsByLocation::<T>::insert(&asset_xcm_location, (asset_id, AssetStatus::Active));
642
136

            
643
136
			T::OnForeignAssetCreated::on_asset_created(&asset_xcm_location, &asset_id);
644
136

            
645
136
			Self::deposit_event(Event::ForeignAssetCreated {
646
136
				contract_address,
647
136
				asset_id,
648
136
				xcm_location: asset_xcm_location,
649
136
				deposit,
650
136
			});
651
136
			Ok(())
652
138
		}
653

            
654
3
		pub fn do_change_xcm_location(
655
3
			asset_id: AssetId,
656
3
			previous_xcm_location: Location,
657
3
			new_xcm_location: Location,
658
3
		) -> DispatchResult {
659
3
			ensure!(
660
3
				!AssetsByLocation::<T>::contains_key(&new_xcm_location),
661
1
				Error::<T>::LocationAlreadyExists
662
			);
663

            
664
			// Remove previous foreign asset info
665
2
			let (_asset_id, asset_status) = AssetsByLocation::<T>::take(&previous_xcm_location)
666
2
				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
667

            
668
			// Insert new foreign asset info
669
2
			AssetsById::<T>::insert(&asset_id, &new_xcm_location);
670
2
			AssetsByLocation::<T>::insert(&new_xcm_location, (asset_id, asset_status));
671
2

            
672
2
			Self::deposit_event(Event::ForeignAssetXcmLocationChanged {
673
2
				asset_id,
674
2
				new_xcm_location,
675
2
				previous_xcm_location,
676
2
			});
677
2
			Ok(())
678
3
		}
679

            
680
9
		pub fn do_freeze_asset(
681
9
			asset_id: AssetId,
682
9
			xcm_location: Location,
683
9
			allow_xcm_deposit: bool,
684
9
		) -> DispatchResult {
685
9
			let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
686
9
				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
687

            
688
9
			ensure!(
689
9
				asset_status == AssetStatus::Active,
690
2
				Error::<T>::AssetAlreadyFrozen
691
			);
692

            
693
7
			EvmCaller::<T>::erc20_pause(asset_id)?;
694

            
695
7
			let new_asset_status = if allow_xcm_deposit {
696
7
				AssetStatus::FrozenXcmDepositAllowed
697
			} else {
698
				AssetStatus::FrozenXcmDepositForbidden
699
			};
700

            
701
7
			AssetsByLocation::<T>::insert(&xcm_location, (asset_id, new_asset_status));
702
7

            
703
7
			Self::deposit_event(Event::ForeignAssetFrozen {
704
7
				asset_id,
705
7
				xcm_location,
706
7
			});
707
7
			Ok(())
708
9
		}
709

            
710
8
		pub fn do_unfreeze_asset(asset_id: AssetId, xcm_location: Location) -> DispatchResult {
711
8
			let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
712
8
				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
713

            
714
8
			ensure!(
715
8
				asset_status == AssetStatus::FrozenXcmDepositAllowed
716
2
					|| asset_status == AssetStatus::FrozenXcmDepositForbidden,
717
2
				Error::<T>::AssetNotFrozen
718
			);
719

            
720
6
			EvmCaller::<T>::erc20_unpause(asset_id)?;
721

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

            
724
6
			Self::deposit_event(Event::ForeignAssetUnfrozen {
725
6
				asset_id,
726
6
				xcm_location,
727
6
			});
728
6
			Ok(())
729
8
		}
730
	}
731

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

            
740
125
			if let AssetStatus::FrozenXcmDepositForbidden = asset_status {
741
				return Err(MatchError::AssetNotHandled.into());
742
125
			}
743

            
744
125
			let beneficiary = T::XcmLocationToH160::convert_location(who)
745
125
				.ok_or(MatchError::AccountIdConversionFailed)?;
746

            
747
			// We perform the evm transfers in a storage transaction to ensure that if it fail
748
			// any contract storage changes are rolled back.
749
125
			frame_support::storage::with_storage_layer(|| {
750
125
				EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount)
751
125
			})?;
752

            
753
125
			Ok(())
754
125
		}
755

            
756
		fn internal_transfer_asset(
757
			asset: &Asset,
758
			from: &Location,
759
			to: &Location,
760
			_context: &XcmContext,
761
		) -> Result<AssetsInHolding, XcmError> {
762
			let (contract_address, amount, asset_status) =
763
				ForeignAssetsMatcher::<T>::match_asset(asset)?;
764

            
765
			if let AssetStatus::FrozenXcmDepositForbidden | AssetStatus::FrozenXcmDepositAllowed =
766
				asset_status
767
			{
768
				return Err(MatchError::AssetNotHandled.into());
769
			}
770

            
771
			let from = T::XcmLocationToH160::convert_location(from)
772
				.ok_or(MatchError::AccountIdConversionFailed)?;
773

            
774
			let to = T::XcmLocationToH160::convert_location(to)
775
				.ok_or(MatchError::AccountIdConversionFailed)?;
776

            
777
			// We perform the evm transfers in a storage transaction to ensure that if it fail
778
			// any contract storage changes are rolled back.
779
			frame_support::storage::with_storage_layer(|| {
780
				EvmCaller::<T>::erc20_transfer(contract_address, from, to, amount)
781
			})?;
782

            
783
			Ok(asset.clone().into())
784
		}
785

            
786
		// Since we don't control the erc20 contract that manages the asset we want to withdraw,
787
		// we can't really withdraw this asset, we can only transfer it to another account.
788
		// It would be possible to transfer the asset to a dedicated account that would reflect
789
		// the content of the xcm holding, but this would imply to perform two evm calls instead of
790
		// one (1 to withdraw the asset and a second one to deposit it).
791
		// In order to perform only one evm call, we just trace the origin of the asset,
792
		// and then the transfer will only really be performed in the deposit instruction.
793
99
		fn withdraw_asset(
794
99
			what: &Asset,
795
99
			who: &Location,
796
99
			_context: Option<&XcmContext>,
797
99
		) -> Result<AssetsInHolding, XcmError> {
798
99
			let (contract_address, amount, asset_status) =
799
99
				ForeignAssetsMatcher::<T>::match_asset(what)?;
800
99
			let who = T::XcmLocationToH160::convert_location(who)
801
99
				.ok_or(MatchError::AccountIdConversionFailed)?;
802

            
803
			if let AssetStatus::FrozenXcmDepositForbidden | AssetStatus::FrozenXcmDepositAllowed =
804
99
				asset_status
805
			{
806
				return Err(MatchError::AssetNotHandled.into());
807
99
			}
808
99

            
809
99
			// We perform the evm transfers in a storage transaction to ensure that if it fail
810
99
			// any contract storage changes are rolled back.
811
99
			frame_support::storage::with_storage_layer(|| {
812
99
				EvmCaller::<T>::erc20_burn_from(contract_address, who, amount)
813
99
			})?;
814

            
815
99
			Ok(what.clone().into())
816
99
		}
817
	}
818

            
819
	impl<T: Config> sp_runtime::traits::MaybeEquivalence<Location, AssetId> for Pallet<T> {
820
		fn convert(location: &Location) -> Option<AssetId> {
821
			AssetsByLocation::<T>::get(location).map(|(asset_id, _)| asset_id)
822
		}
823
97
		fn convert_back(asset_id: &AssetId) -> Option<Location> {
824
97
			AssetsById::<T>::get(asset_id)
825
97
		}
826
	}
827
}