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

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

            
151
#[derive(Decode, Debug, Encode, PartialEq, TypeInfo)]
152
pub enum AssetStatus {
153
	/// All operations are enabled
154
	Active,
155
	/// The asset is frozen, but deposit from XCM still work
156
	FrozenXcmDepositAllowed,
157
	/// The asset is frozen, and deposit from XCM will fail
158
	FrozenXcmDepositForbidden,
159
}
160

            
161
impl AssetStatus {
162
1690
	pub fn is_frozen(&self) -> bool {
163
1613
		matches!(
164
1690
			self,
165
			AssetStatus::FrozenXcmDepositAllowed | AssetStatus::FrozenXcmDepositForbidden
166
		)
167
1690
	}
168
}
169

            
170
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171
pub struct EvmForeignAssetInfo {
172
	pub asset_id: AssetId,
173
	pub xcm_location: Location,
174
	pub decimals: u8,
175
	pub symbol: BoundedVec<u8, ConstU32<256>>,
176
	pub name: BoundedVec<u8, ConstU32<256>>,
177
}
178

            
179
#[pallet]
180
pub mod pallet {
181
	use super::*;
182
	use frame_support::traits::{Currency, ReservableCurrency};
183
	use pallet_evm::{GasWeightMapping, Runner};
184
	use sp_runtime::traits::{AccountIdConversion, AtLeast32BitUnsigned, Convert};
185
	use xcm_executor::traits::ConvertLocation;
186
	use xcm_executor::traits::Error as MatchError;
187
	use xcm_executor::AssetsInHolding;
188

            
189
	#[pallet::pallet]
190
	#[pallet::without_storage_info]
191
	pub struct Pallet<T>(PhantomData<T>);
192

            
193
	/// The moonbeam foreign assets's pallet id
194
	pub const PALLET_ID: frame_support::PalletId = frame_support::PalletId(*b"forgasst");
195

            
196
	#[pallet::config]
197
	pub trait Config:
198
		frame_system::Config<RuntimeEvent: From<Event<Self>>>
199
		+ pallet_evm::Config
200
		+ scale_info::TypeInfo
201
	{
202
		// Convert AccountId to H160
203
		type AccountIdToH160: Convert<Self::AccountId, H160>;
204

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

            
208
		/// EVM runner
209
		type EvmRunner: Runner<Self>;
210

            
211
		type ConvertLocation: ConvertLocation<Self::AccountId>;
212

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

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

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

            
222
		/// Origin that is allowed to unfreeze all tokens of a foreign asset that was previously
223
		/// frozen
224
		type ForeignAssetUnfreezerOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = OriginType>;
225

            
226
		/// Hook to be called when new foreign asset is registered.
227
		type OnForeignAssetCreated: ForeignAssetCreatedHook<Location>;
228

            
229
		/// Maximum numbers of different foreign assets
230
		type MaxForeignAssets: Get<u32>;
231

            
232
		/// Weight information for extrinsics in this pallet.
233
		type WeightInfo: WeightInfo;
234

            
235
		// Convert XCM Location to H160
236
		type XcmLocationToH160: ConvertLocation<H160>;
237

            
238
		/// Amount of tokens required to lock for creating a new foreign asset
239
		type ForeignAssetCreationDeposit: Get<BalanceOf<Self>>;
240

            
241
		/// The balance type for locking funds
242
		type Balance: Member
243
			+ Parameter
244
			+ AtLeast32BitUnsigned
245
			+ Default
246
			+ Copy
247
			+ MaybeSerializeDeserialize
248
			+ MaxEncodedLen
249
			+ TypeInfo;
250

            
251
		/// The currency type for locking funds
252
		type Currency: ReservableCurrency<Self::AccountId>;
253
	}
254

            
255
	type BalanceOf<T> =
256
		<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
257

            
258
	pub type AssetBalance = U256;
259
	pub type AssetId = u128;
260

            
261
	/// An error that can occur while executing the mapping pallet's logic.
262
	#[pallet::error]
263
	pub enum Error<T> {
264
		AssetAlreadyExists,
265
		AssetAlreadyFrozen,
266
		AssetDoesNotExist,
267
		AssetIdFiltered,
268
		AssetNotFrozen,
269
		CorruptedStorageOrphanLocation,
270
		Erc20ContractCreationFail,
271
		EvmCallPauseFail,
272
		EvmCallUnpauseFail,
273
		EvmCallMintIntoFail,
274
		EvmCallTransferFail,
275
		EvmInternalError,
276
		/// Account has insufficient balance for locking
277
		InsufficientBalance,
278
		CannotConvertLocationToAccount,
279
		LocationOutsideOfOrigin,
280
		AssetNotInSiblingPara,
281
		InvalidSymbol,
282
		InvalidTokenName,
283
		LocationAlreadyExists,
284
		NoPendingDeposit,
285
		AssetNotActive,
286
		TooManyForeignAssets,
287
	}
288

            
289
	#[pallet::event]
290
	#[pallet::generate_deposit(pub(crate) fn deposit_event)]
291
	pub enum Event<T: Config> {
292
		/// New asset with the asset manager is registered
293
		ForeignAssetCreated {
294
			contract_address: H160,
295
			asset_id: AssetId,
296
			xcm_location: Location,
297
			deposit: Option<BalanceOf<T>>,
298
		},
299
		/// Changed the xcm type mapping for a given asset id
300
		ForeignAssetXcmLocationChanged {
301
			asset_id: AssetId,
302
			previous_xcm_location: Location,
303
			new_xcm_location: Location,
304
		},
305
		// Freezes all tokens of a given asset id
306
		ForeignAssetFrozen {
307
			asset_id: AssetId,
308
			xcm_location: Location,
309
		},
310
		// Thawing a previously frozen asset
311
		ForeignAssetUnfrozen {
312
			asset_id: AssetId,
313
			xcm_location: Location,
314
		},
315
		/// Tokens have been locked for asset creation
316
		TokensLocked(T::AccountId, AssetId, AssetBalance),
317
		/// A deposit was recorded as pending because the asset is frozen
318
		PendingDepositRecorded {
319
			asset_id: AssetId,
320
			beneficiary: H160,
321
			amount: U256,
322
			total_pending: U256,
323
		},
324
		/// A pending deposit was claimed and minted
325
		PendingDepositClaimed {
326
			asset_id: AssetId,
327
			beneficiary: H160,
328
			amount: U256,
329
		},
330
	}
331

            
332
	/// Mapping from an asset id to a Foreign asset type.
333
	/// This is mostly used when receiving transaction specifying an asset directly,
334
	/// like transferring an asset from this chain to another.
335
	#[pallet::storage]
336
	#[pallet::getter(fn assets_by_id)]
337
	pub type AssetsById<T: Config> =
338
		CountedStorageMap<_, Blake2_128Concat, AssetId, Location, OptionQuery>;
339

            
340
	/// Reverse mapping of AssetsById. Mapping from a foreign asset to an asset id.
341
	/// This is mostly used when receiving a multilocation XCM message to retrieve
342
	/// the corresponding asset in which tokens should me minted.
343
	#[pallet::storage]
344
	#[pallet::getter(fn assets_by_location)]
345
	pub type AssetsByLocation<T: Config> =
346
		StorageMap<_, Blake2_128Concat, Location, (AssetId, AssetStatus)>;
347

            
348
	/// Mapping from an asset id to its creation details
349
	#[pallet::storage]
350
	#[pallet::getter(fn assets_creation_details)]
351
	pub type AssetsCreationDetails<T: Config> =
352
		StorageMap<_, Blake2_128Concat, AssetId, AssetDepositDetails<T>>;
353

            
354
	/// Pending deposits for frozen assets, keyed by (asset_id, beneficiary).
355
	/// Deposits for the same (asset_id, beneficiary) accumulate via checked_add.
356
	#[pallet::storage]
357
	#[pallet::getter(fn pending_deposits)]
358
	pub type PendingDeposits<T: Config> =
359
		StorageDoubleMap<_, Blake2_128Concat, AssetId, Blake2_128Concat, H160, U256, OptionQuery>;
360

            
361
	#[derive(Clone, Decode, Encode, Eq, PartialEq, Debug, TypeInfo, MaxEncodedLen)]
362
	pub struct AssetDepositDetails<T: Config> {
363
		pub deposit_account: T::AccountId,
364
		pub deposit: BalanceOf<T>,
365
	}
366

            
367
	#[pallet::genesis_config]
368
	pub struct GenesisConfig<T: Config> {
369
		pub assets: Vec<EvmForeignAssetInfo>,
370
		pub _phantom: PhantomData<T>,
371
	}
372

            
373
	impl<T: Config> Default for GenesisConfig<T> {
374
6
		fn default() -> Self {
375
6
			Self {
376
6
				assets: vec![],
377
6
				_phantom: Default::default(),
378
6
			}
379
6
		}
380
	}
381

            
382
	#[pallet::genesis_build]
383
	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
384
6
		fn build(&self) {
385
6
			for asset in self.assets.clone() {
386
				Pallet::<T>::register_foreign_asset(
387
					asset.asset_id,
388
					asset.xcm_location,
389
					asset.decimals,
390
					asset.symbol,
391
					asset.name,
392
				)
393
				.expect("couldn't register asset");
394
			}
395
6
		}
396
	}
397

            
398
	impl<T: Config> Pallet<T> {
399
		/// The account ID of this pallet
400
		#[inline]
401
723
		pub fn account_id() -> H160 {
402
723
			let account_id: T::AccountId = PALLET_ID.into_account_truncating();
403
723
			T::AccountIdToH160::convert(account_id)
404
723
		}
405

            
406
		/// Compute asset contract address from asset id
407
		#[inline]
408
596
		pub fn contract_address_from_asset_id(asset_id: AssetId) -> H160 {
409
596
			let mut buffer = [0u8; 20];
410
596
			buffer[..4].copy_from_slice(&FOREIGN_ASSETS_PREFIX);
411
596
			buffer[4..].copy_from_slice(&asset_id.to_be_bytes());
412
596
			H160(buffer)
413
596
		}
414

            
415
		/// This method only exists for migration purposes and will be deleted once the
416
		/// foreign assets migration is finished.
417
103
		pub fn register_foreign_asset(
418
103
			asset_id: AssetId,
419
103
			xcm_location: Location,
420
103
			decimals: u8,
421
103
			symbol: BoundedVec<u8, ConstU32<256>>,
422
103
			name: BoundedVec<u8, ConstU32<256>>,
423
103
		) -> DispatchResult {
424
103
			Self::do_create_asset(asset_id, xcm_location, decimals, symbol, name, None)
425
103
		}
426

            
427
		/// Mint an asset into a specific account
428
16
		pub fn mint_into(
429
16
			asset_id: AssetId,
430
16
			beneficiary: T::AccountId,
431
16
			amount: U256,
432
16
		) -> Result<(), evm::EvmError> {
433
			// We perform the evm call in a storage transaction to ensure that if it fail
434
			// any contract storage changes are rolled back.
435
16
			frame_support::storage::with_storage_layer(|| {
436
16
				EvmCaller::<T>::erc20_mint_into(
437
16
					Self::contract_address_from_asset_id(asset_id),
438
16
					T::AccountIdToH160::convert(beneficiary),
439
16
					amount,
440
				)
441
16
			})
442
16
			.map_err(Into::into)
443
16
		}
444

            
445
		/// Transfer an asset from an account to another one
446
7
		pub fn transfer(
447
7
			asset_id: AssetId,
448
7
			from: T::AccountId,
449
7
			to: T::AccountId,
450
7
			amount: U256,
451
7
		) -> Result<(), evm::EvmError> {
452
7
			frame_support::storage::with_storage_layer(|| {
453
7
				EvmCaller::<T>::erc20_transfer(
454
7
					Self::contract_address_from_asset_id(asset_id),
455
7
					T::AccountIdToH160::convert(from),
456
7
					T::AccountIdToH160::convert(to),
457
7
					amount,
458
				)
459
7
			})
460
7
			.map_err(Into::into)
461
7
		}
462

            
463
181
		pub fn balance(asset_id: AssetId, who: T::AccountId) -> Result<U256, evm::EvmError> {
464
181
			EvmCaller::<T>::erc20_balance_of(asset_id, T::AccountIdToH160::convert(who))
465
181
				.map_err(Into::into)
466
181
		}
467

            
468
		/// Approve a spender to spend a certain amount of tokens from the owner account
469
		pub fn approve(
470
			asset_id: AssetId,
471
			owner: T::AccountId,
472
			spender: T::AccountId,
473
			amount: U256,
474
		) -> Result<(), evm::EvmError> {
475
			// We perform the evm call in a storage transaction to ensure that if it fail
476
			// any contract storage changes are rolled back.
477
			frame_support::storage::with_storage_layer(|| {
478
				EvmCaller::<T>::erc20_approve(
479
					Self::contract_address_from_asset_id(asset_id),
480
					T::AccountIdToH160::convert(owner),
481
					T::AccountIdToH160::convert(spender),
482
					amount,
483
				)
484
			})
485
			.map_err(Into::into)
486
		}
487

            
488
		pub fn weight_of_erc20_burn() -> Weight {
489
			T::GasWeightMapping::gas_to_weight(evm::ERC20_BURN_FROM_GAS_LIMIT, true)
490
		}
491
		pub fn weight_of_erc20_mint() -> Weight {
492
			T::GasWeightMapping::gas_to_weight(evm::ERC20_MINT_INTO_GAS_LIMIT, true)
493
		}
494
		pub fn weight_of_erc20_transfer() -> Weight {
495
			T::GasWeightMapping::gas_to_weight(evm::ERC20_TRANSFER_GAS_LIMIT, true)
496
		}
497
		#[cfg(feature = "runtime-benchmarks")]
498
		pub fn set_asset(asset_location: Location, asset_id: AssetId) {
499
			AssetsByLocation::<T>::insert(&asset_location, (asset_id, AssetStatus::Active));
500
			AssetsById::<T>::insert(&asset_id, asset_location);
501
		}
502

            
503
		#[cfg(feature = "runtime-benchmarks")]
504
		pub fn create_asset_contract(
505
			asset_id: AssetId,
506
			decimals: u8,
507
			symbol: &str,
508
			name: &str,
509
		) -> Result<H160, Error<T>> {
510
			EvmCaller::<T>::erc20_create(asset_id, decimals, symbol, name)
511
		}
512
	}
513

            
514
	#[pallet::call]
515
	impl<T: Config> Pallet<T> {
516
		/// Create new asset with the ForeignAssetCreator
517
		#[pallet::call_index(0)]
518
		#[pallet::weight(<T as Config>::WeightInfo::create_foreign_asset())]
519
		pub fn create_foreign_asset(
520
			origin: OriginFor<T>,
521
			asset_id: AssetId,
522
			asset_xcm_location: Location,
523
			decimals: u8,
524
			symbol: BoundedVec<u8, ConstU32<256>>,
525
			name: BoundedVec<u8, ConstU32<256>>,
526
39
		) -> DispatchResult {
527
39
			let origin_type = T::ForeignAssetCreatorOrigin::ensure_origin(origin.clone())?;
528

            
529
38
			Self::ensure_origin_can_modify_location(origin_type.clone(), &asset_xcm_location)?;
530
38
			let deposit_account = Self::get_deposit_account(origin_type)?;
531

            
532
38
			Self::do_create_asset(
533
38
				asset_id,
534
38
				asset_xcm_location,
535
38
				decimals,
536
38
				symbol,
537
38
				name,
538
38
				deposit_account,
539
			)
540
		}
541

            
542
		/// Change the xcm type mapping for a given assetId
543
		/// We also change this if the previous units per second where pointing at the old
544
		/// assetType
545
		#[pallet::call_index(1)]
546
		#[pallet::weight(<T as Config>::WeightInfo::change_xcm_location())]
547
		pub fn change_xcm_location(
548
			origin: OriginFor<T>,
549
			asset_id: AssetId,
550
			new_xcm_location: Location,
551
6
		) -> DispatchResult {
552
6
			let origin_type = T::ForeignAssetModifierOrigin::ensure_origin(origin.clone())?;
553

            
554
5
			Self::ensure_origin_can_modify_location(origin_type.clone(), &new_xcm_location)?;
555

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

            
559
4
			Self::ensure_origin_can_modify_location(origin_type, &previous_location)?;
560

            
561
3
			Self::do_change_xcm_location(asset_id, previous_location, new_xcm_location)
562
		}
563

            
564
		/// Freeze a given foreign assetId
565
		#[pallet::call_index(2)]
566
		#[pallet::weight(<T as Config>::WeightInfo::freeze_foreign_asset())]
567
		pub fn freeze_foreign_asset(
568
			origin: OriginFor<T>,
569
			asset_id: AssetId,
570
			allow_xcm_deposit: bool,
571
17
		) -> DispatchResult {
572
17
			let origin_type = T::ForeignAssetFreezerOrigin::ensure_origin(origin.clone())?;
573

            
574
17
			let xcm_location =
575
17
				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
576

            
577
17
			Self::ensure_origin_can_modify_location(origin_type, &xcm_location)?;
578

            
579
15
			Self::do_freeze_asset(asset_id, xcm_location, allow_xcm_deposit)
580
		}
581

            
582
		/// Unfreeze a given foreign assetId
583
		#[pallet::call_index(3)]
584
		#[pallet::weight(<T as Config>::WeightInfo::unfreeze_foreign_asset())]
585
9
		pub fn unfreeze_foreign_asset(origin: OriginFor<T>, asset_id: AssetId) -> DispatchResult {
586
9
			let origin_type = T::ForeignAssetUnfreezerOrigin::ensure_origin(origin.clone())?;
587

            
588
9
			let xcm_location =
589
9
				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
590

            
591
9
			Self::ensure_origin_can_modify_location(origin_type, &xcm_location)?;
592

            
593
9
			Self::do_unfreeze_asset(asset_id, xcm_location)
594
		}
595

            
596
		/// Claim a pending deposit for a given asset and beneficiary.
597
		/// Callable by any signed origin (permissionless). Tokens are minted to the
598
		/// beneficiary, not the caller. Requires the asset to be active (unfrozen).
599
		#[pallet::call_index(4)]
600
		#[pallet::weight(<T as Config>::WeightInfo::claim_pending_deposit())]
601
		pub fn claim_pending_deposit(
602
			origin: OriginFor<T>,
603
			asset_id: AssetId,
604
			beneficiary: H160,
605
3
		) -> DispatchResult {
606
3
			ensure_signed(origin)?;
607

            
608
3
			let xcm_location =
609
3
				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
610
3
			let (_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
611
3
				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
612

            
613
3
			ensure!(
614
3
				asset_status == AssetStatus::Active,
615
1
				Error::<T>::AssetNotActive
616
			);
617

            
618
2
			let amount = PendingDeposits::<T>::get(asset_id, beneficiary)
619
2
				.ok_or(Error::<T>::NoPendingDeposit)?;
620

            
621
1
			let contract_address = Self::contract_address_from_asset_id(asset_id);
622

            
623
			// Both the storage removal and the EVM mint run inside the same
624
			// transactional layer so they are rolled back atomically on failure.
625
1
			frame_support::storage::with_storage_layer(|| {
626
1
				PendingDeposits::<T>::remove(asset_id, beneficiary);
627
1
				EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount)
628
1
			})
629
1
			.map_err(|_| Error::<T>::EvmCallMintIntoFail)?;
630

            
631
1
			Self::deposit_event(Event::PendingDepositClaimed {
632
1
				asset_id,
633
1
				beneficiary,
634
1
				amount,
635
1
			});
636

            
637
1
			Ok(())
638
		}
639
	}
640

            
641
	impl<T: Config> Pallet<T> {
642
		/// Ensure that the caller origin can modify the location,
643
73
		fn ensure_origin_can_modify_location(
644
73
			origin_type: OriginType,
645
73
			location: &Location,
646
73
		) -> DispatchResult {
647
73
			match origin_type {
648
17
				OriginType::XCM(origin_location) => {
649
17
					ensure!(
650
17
						location.starts_with(&origin_location),
651
3
						Error::<T>::LocationOutsideOfOrigin,
652
					);
653
				}
654
56
				OriginType::Governance => {
655
56
					// nothing to check Governance can change any asset
656
56
				}
657
			};
658
70
			Ok(())
659
73
		}
660

            
661
38
		fn get_deposit_account(
662
38
			origin_type: OriginType,
663
38
		) -> Result<Option<T::AccountId>, DispatchError> {
664
38
			match origin_type {
665
4
				OriginType::XCM(origin_location) => {
666
4
					let deposit_account = convert_location::<T>(&origin_location)?;
667
4
					Ok(Some(deposit_account))
668
				}
669
34
				OriginType::Governance => Ok(None),
670
			}
671
38
		}
672

            
673
141
		pub fn do_create_asset(
674
141
			asset_id: AssetId,
675
141
			asset_xcm_location: Location,
676
141
			decimals: u8,
677
141
			symbol: BoundedVec<u8, ConstU32<256>>,
678
141
			name: BoundedVec<u8, ConstU32<256>>,
679
141
			deposit_account: Option<T::AccountId>,
680
141
		) -> DispatchResult {
681
141
			ensure!(
682
141
				!AssetsById::<T>::contains_key(&asset_id),
683
1
				Error::<T>::AssetAlreadyExists
684
			);
685

            
686
140
			ensure!(
687
140
				!AssetsByLocation::<T>::contains_key(&asset_xcm_location),
688
1
				Error::<T>::LocationAlreadyExists
689
			);
690

            
691
139
			ensure!(
692
139
				AssetsById::<T>::count() < T::MaxForeignAssets::get(),
693
				Error::<T>::TooManyForeignAssets
694
			);
695

            
696
139
			ensure!(
697
139
				T::AssetIdFilter::contains(&asset_id),
698
				Error::<T>::AssetIdFiltered
699
			);
700

            
701
139
			let symbol = core::str::from_utf8(&symbol).map_err(|_| Error::<T>::InvalidSymbol)?;
702
139
			let name = core::str::from_utf8(&name).map_err(|_| Error::<T>::InvalidTokenName)?;
703
139
			let contract_address = EvmCaller::<T>::erc20_create(asset_id, decimals, symbol, name)?;
704

            
705
139
			let deposit = if let Some(deposit_account) = deposit_account {
706
4
				let deposit = T::ForeignAssetCreationDeposit::get();
707

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

            
711
				// Insert the amount that is reserved from the user
712
4
				AssetsCreationDetails::<T>::insert(
713
4
					&asset_id,
714
4
					AssetDepositDetails {
715
4
						deposit_account,
716
4
						deposit,
717
4
					},
718
				);
719

            
720
4
				Some(deposit)
721
			} else {
722
135
				None
723
			};
724

            
725
			// Insert the association assetId->foreigAsset
726
			// Insert the association foreigAsset->assetId
727
139
			AssetsById::<T>::insert(&asset_id, &asset_xcm_location);
728
139
			AssetsByLocation::<T>::insert(&asset_xcm_location, (asset_id, AssetStatus::Active));
729

            
730
139
			T::OnForeignAssetCreated::on_asset_created(&asset_xcm_location, &asset_id);
731

            
732
139
			Self::deposit_event(Event::ForeignAssetCreated {
733
139
				contract_address,
734
139
				asset_id,
735
139
				xcm_location: asset_xcm_location,
736
139
				deposit,
737
139
			});
738
139
			Ok(())
739
141
		}
740

            
741
3
		pub fn do_change_xcm_location(
742
3
			asset_id: AssetId,
743
3
			previous_xcm_location: Location,
744
3
			new_xcm_location: Location,
745
3
		) -> DispatchResult {
746
3
			ensure!(
747
3
				!AssetsByLocation::<T>::contains_key(&new_xcm_location),
748
1
				Error::<T>::LocationAlreadyExists
749
			);
750

            
751
			// Remove previous foreign asset info
752
2
			let (_asset_id, asset_status) = AssetsByLocation::<T>::take(&previous_xcm_location)
753
2
				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
754

            
755
			// Insert new foreign asset info
756
2
			AssetsById::<T>::insert(&asset_id, &new_xcm_location);
757
2
			AssetsByLocation::<T>::insert(&new_xcm_location, (asset_id, asset_status));
758

            
759
2
			Self::deposit_event(Event::ForeignAssetXcmLocationChanged {
760
2
				asset_id,
761
2
				new_xcm_location,
762
2
				previous_xcm_location,
763
2
			});
764
2
			Ok(())
765
3
		}
766

            
767
15
		pub fn do_freeze_asset(
768
15
			asset_id: AssetId,
769
15
			xcm_location: Location,
770
15
			allow_xcm_deposit: bool,
771
15
		) -> DispatchResult {
772
15
			let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
773
15
				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
774

            
775
15
			ensure!(!asset_status.is_frozen(), Error::<T>::AssetAlreadyFrozen);
776

            
777
13
			EvmCaller::<T>::erc20_pause(asset_id)?;
778

            
779
13
			let new_asset_status = if allow_xcm_deposit {
780
12
				AssetStatus::FrozenXcmDepositAllowed
781
			} else {
782
1
				AssetStatus::FrozenXcmDepositForbidden
783
			};
784

            
785
13
			AssetsByLocation::<T>::insert(&xcm_location, (asset_id, new_asset_status));
786

            
787
13
			Self::deposit_event(Event::ForeignAssetFrozen {
788
13
				asset_id,
789
13
				xcm_location,
790
13
			});
791
13
			Ok(())
792
15
		}
793

            
794
9
		pub fn do_unfreeze_asset(asset_id: AssetId, xcm_location: Location) -> DispatchResult {
795
9
			let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
796
9
				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
797

            
798
9
			ensure!(asset_status.is_frozen(), Error::<T>::AssetNotFrozen);
799

            
800
7
			EvmCaller::<T>::erc20_unpause(asset_id)?;
801

            
802
7
			AssetsByLocation::<T>::insert(&xcm_location, (asset_id, AssetStatus::Active));
803

            
804
7
			Self::deposit_event(Event::ForeignAssetUnfrozen {
805
7
				asset_id,
806
7
				xcm_location,
807
7
			});
808
7
			Ok(())
809
9
		}
810
	}
811

            
812
	impl<T: Config> xcm_executor::traits::TransactAsset for Pallet<T> {
813
		// For optimization reasons, the asset we want to deposit has not really been withdrawn,
814
		// we have just traced from which account it should have been withdrawn.
815
		// So we will retrieve these information and make the transfer from the origin account.
816
133
		fn deposit_asset(what: &Asset, who: &Location, _context: Option<&XcmContext>) -> XcmResult {
817
133
			let (asset_id, contract_address, amount, asset_status) =
818
133
				ForeignAssetsMatcher::<T>::match_asset(what)?;
819

            
820
133
			if let AssetStatus::FrozenXcmDepositForbidden = asset_status {
821
1
				return Err(XcmError::FailedToTransactAsset(
822
1
					"asset is frozen and XCM deposits are forbidden",
823
1
				));
824
132
			}
825

            
826
132
			let beneficiary = T::XcmLocationToH160::convert_location(who)
827
132
				.ok_or(MatchError::AccountIdConversionFailed)?;
828

            
829
132
			if matches!(asset_status, AssetStatus::FrozenXcmDepositAllowed) {
830
6
				let total_pending = PendingDeposits::<T>::get(asset_id, beneficiary)
831
6
					.unwrap_or(U256::zero())
832
6
					.checked_add(amount)
833
6
					.ok_or(XcmError::Overflow)?;
834

            
835
5
				PendingDeposits::<T>::insert(asset_id, beneficiary, total_pending);
836

            
837
5
				Pallet::<T>::deposit_event(Event::PendingDepositRecorded {
838
5
					asset_id,
839
5
					beneficiary,
840
5
					amount,
841
5
					total_pending,
842
5
				});
843
			} else {
844
				// We perform the evm transfers in a storage transaction to ensure
845
				// that if it fails any contract storage changes are rolled back.
846
126
				frame_support::storage::with_storage_layer(|| {
847
126
					EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount)
848
126
				})?;
849
			}
850

            
851
131
			Ok(())
852
133
		}
853

            
854
		fn internal_transfer_asset(
855
			asset: &Asset,
856
			from: &Location,
857
			to: &Location,
858
			_context: &XcmContext,
859
		) -> Result<AssetsInHolding, XcmError> {
860
			let (_asset_id, contract_address, amount, asset_status) =
861
				ForeignAssetsMatcher::<T>::match_asset(asset)?;
862

            
863
			if asset_status.is_frozen() {
864
				return Err(XcmError::FailedToTransactAsset("asset is frozen"));
865
			}
866

            
867
			let from = T::XcmLocationToH160::convert_location(from)
868
				.ok_or(MatchError::AccountIdConversionFailed)?;
869

            
870
			let to = T::XcmLocationToH160::convert_location(to)
871
				.ok_or(MatchError::AccountIdConversionFailed)?;
872

            
873
			// We perform the evm transfers in a storage transaction to ensure that if it fail
874
			// any contract storage changes are rolled back.
875
			frame_support::storage::with_storage_layer(|| {
876
				EvmCaller::<T>::erc20_transfer(contract_address, from, to, amount)
877
			})?;
878

            
879
			Ok(asset.clone().into())
880
		}
881

            
882
		// Since we don't control the erc20 contract that manages the asset we want to withdraw,
883
		// we can't really withdraw this asset, we can only transfer it to another account.
884
		// It would be possible to transfer the asset to a dedicated account that would reflect
885
		// the content of the xcm holding, but this would imply to perform two evm calls instead of
886
		// one (1 to withdraw the asset and a second one to deposit it).
887
		// In order to perform only one evm call, we just trace the origin of the asset,
888
		// and then the transfer will only really be performed in the deposit instruction.
889
97
		fn withdraw_asset(
890
97
			what: &Asset,
891
97
			who: &Location,
892
97
			_context: Option<&XcmContext>,
893
97
		) -> Result<AssetsInHolding, XcmError> {
894
97
			let (_asset_id, contract_address, amount, asset_status) =
895
97
				ForeignAssetsMatcher::<T>::match_asset(what)?;
896
97
			let who = T::XcmLocationToH160::convert_location(who)
897
97
				.ok_or(MatchError::AccountIdConversionFailed)?;
898

            
899
97
			if asset_status.is_frozen() {
900
				return Err(XcmError::FailedToTransactAsset("asset is frozen"));
901
97
			}
902

            
903
			// We perform the evm transfers in a storage transaction to ensure that if it fail
904
			// any contract storage changes are rolled back.
905
97
			frame_support::storage::with_storage_layer(|| {
906
97
				EvmCaller::<T>::erc20_burn_from(contract_address, who, amount)
907
97
			})?;
908

            
909
97
			Ok(what.clone().into())
910
97
		}
911

            
912
		#[cfg(feature = "runtime-benchmarks")]
913
		fn can_check_out(_dest: &Location, _what: &Asset, _context: &XcmContext) -> XcmResult {
914
			// Needed for the benchmarks to work
915
			Ok(())
916
		}
917

            
918
		#[cfg(feature = "runtime-benchmarks")]
919
		fn check_out(_dest: &Location, _what: &Asset, _context: &XcmContext) {
920
			// Needed for benchmarks to work
921
		}
922
	}
923

            
924
	impl<T: Config> sp_runtime::traits::MaybeEquivalence<Location, AssetId> for Pallet<T> {
925
		fn convert(location: &Location) -> Option<AssetId> {
926
			AssetsByLocation::<T>::get(location).map(|(asset_id, _)| asset_id)
927
		}
928
91
		fn convert_back(asset_id: &AssetId) -> Option<Location> {
929
91
			AssetsById::<T>::get(asset_id)
930
91
		}
931
	}
932
}