1
// Copyright 2024 Moonbeam Foundation.
2
// This file is part of Moonbeam.
3

            
4
// Moonbeam is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Moonbeam is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Moonbeam.  If not, see <http://www.gnu.org/licenses/>.
16

            
17
//! # Moonbeam Foreign Assets pallet
18
//!
19
//! This pallets allow to create and manage XCM derivative assets (aka. foreign assets).
20
//!
21
//! Each asset is implemented by an evm smart contract that is deployed by this pallet
22
//! The evm smart contract for each asset is trusted by the runtime, and should
23
//! be deployed only by the runtime itself.
24
//!
25
//! This pallet made several assumptions on theses evm smarts contracts:
26
//! - Only this pallet should be able to mint and burn tokens
27
//! - The following selectors should be exposed and callable only by this pallet account:
28
//!   - burnFrom(address, uint256)
29
//!   - mintInto(address, uint256)
30
//!   - pause(address, uint256)
31
//!   - unpause(address, uint256)
32
//! - The smart contract should expose as weel the ERC20.transfer selector
33
//!
34
//! Each asset has a unique identifier that can never change.
35
//! This identifier is named "AssetId", it's an integer (u128).
36
//! This pallet maintain a two-way mapping beetween each AssetId the XCM Location of the asset.
37

            
38
#![cfg_attr(not(feature = "std"), no_std)]
39

            
40
#[cfg(any(test, feature = "runtime-benchmarks"))]
41
pub mod benchmarks;
42
#[cfg(test)]
43
pub mod mock;
44
#[cfg(test)]
45
pub mod tests;
46
pub mod weights;
47

            
48
mod evm;
49

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
245
		/// Mint an asset into a specific account
246
4
		pub fn mint_into(
247
4
			asset_id: AssetId,
248
4
			beneficiary: T::AccountId,
249
4
			amount: U256,
250
4
		) -> Result<(), evm::EvmError> {
251
4
			// We perform the evm call in a storage transaction to ensure that if it fail
252
4
			// any contract storage changes are rolled back.
253
4
			frame_support::storage::with_storage_layer(|| {
254
4
				EvmCaller::<T>::erc20_mint_into(
255
4
					Self::contract_address_from_asset_id(asset_id),
256
4
					T::AccountIdToH160::convert(beneficiary),
257
4
					amount,
258
4
				)
259
4
			})
260
4
			.map_err(Into::into)
261
4
		}
262
104
		pub fn weight_of_erc20_burn() -> Weight {
263
104
			T::GasWeightMapping::gas_to_weight(evm::ERC20_BURN_FROM_GAS_LIMIT, true)
264
104
		}
265
		pub fn weight_of_erc20_mint() -> Weight {
266
			T::GasWeightMapping::gas_to_weight(evm::ERC20_MINT_INTO_GAS_LIMIT, true)
267
		}
268
20
		pub fn weight_of_erc20_transfer() -> Weight {
269
20
			T::GasWeightMapping::gas_to_weight(evm::ERC20_TRANSFER_GAS_LIMIT, true)
270
20
		}
271
		#[cfg(feature = "runtime-benchmarks")]
272
		pub fn set_asset(asset_location: Location, asset_id: AssetId) {
273
			AssetsByLocation::<T>::insert(&asset_location, (asset_id, AssetStatus::Active));
274
			AssetsById::<T>::insert(&asset_id, asset_location);
275
		}
276
	}
277

            
278
	#[pallet::call]
279
	impl<T: Config> Pallet<T> {
280
		/// Create new asset with the ForeignAssetCreator
281
		#[pallet::call_index(0)]
282
		#[pallet::weight(<T as Config>::WeightInfo::create_foreign_asset())]
283
		pub fn create_foreign_asset(
284
			origin: OriginFor<T>,
285
			asset_id: AssetId,
286
			xcm_location: Location,
287
			decimals: u8,
288
			symbol: BoundedVec<u8, ConstU32<256>>,
289
			name: BoundedVec<u8, ConstU32<256>>,
290
13
		) -> DispatchResult {
291
13
			T::ForeignAssetCreatorOrigin::ensure_origin(origin)?;
292

            
293
			// Ensure such an assetId does not exist
294
12
			ensure!(
295
12
				!AssetsById::<T>::contains_key(&asset_id),
296
1
				Error::<T>::AssetAlreadyExists
297
			);
298

            
299
11
			ensure!(
300
11
				!AssetsByLocation::<T>::contains_key(&xcm_location),
301
1
				Error::<T>::LocationAlreadyExists
302
			);
303

            
304
10
			ensure!(
305
10
				AssetsById::<T>::count() < T::MaxForeignAssets::get(),
306
				Error::<T>::TooManyForeignAssets
307
			);
308

            
309
10
			ensure!(
310
10
				T::AssetIdFilter::contains(&asset_id),
311
				Error::<T>::AssetIdFiltered
312
			);
313

            
314
10
			let symbol = core::str::from_utf8(&symbol).map_err(|_| Error::<T>::InvalidSymbol)?;
315
10
			let name = core::str::from_utf8(&name).map_err(|_| Error::<T>::InvalidTokenName)?;
316

            
317
10
			let contract_address = EvmCaller::<T>::erc20_create(asset_id, decimals, symbol, name)?;
318

            
319
			// Insert the association assetId->foreigAsset
320
			// Insert the association foreigAsset->assetId
321
10
			AssetsById::<T>::insert(&asset_id, &xcm_location);
322
10
			AssetsByLocation::<T>::insert(&xcm_location, (asset_id, AssetStatus::Active));
323
10

            
324
10
			T::OnForeignAssetCreated::on_asset_created(&xcm_location, &asset_id);
325
10

            
326
10
			Self::deposit_event(Event::ForeignAssetCreated {
327
10
				contract_address,
328
10
				asset_id,
329
10
				xcm_location,
330
10
			});
331
10
			Ok(())
332
		}
333

            
334
		/// Change the xcm type mapping for a given assetId
335
		/// We also change this if the previous units per second where pointing at the old
336
		/// assetType
337
		#[pallet::call_index(1)]
338
		#[pallet::weight(<T as Config>::WeightInfo::change_xcm_location())]
339
		pub fn change_xcm_location(
340
			origin: OriginFor<T>,
341
			asset_id: AssetId,
342
			new_xcm_location: Location,
343
4
		) -> DispatchResult {
344
4
			T::ForeignAssetModifierOrigin::ensure_origin(origin)?;
345

            
346
2
			let previous_location =
347
3
				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
348

            
349
2
			ensure!(
350
2
				!AssetsByLocation::<T>::contains_key(&new_xcm_location),
351
1
				Error::<T>::LocationAlreadyExists
352
			);
353

            
354
			// Remove previous foreign asset info
355
1
			let (_asset_id, asset_status) = AssetsByLocation::<T>::take(&previous_location)
356
1
				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
357

            
358
			// Insert new foreign asset info
359
1
			AssetsById::<T>::insert(&asset_id, &new_xcm_location);
360
1
			AssetsByLocation::<T>::insert(&new_xcm_location, (asset_id, asset_status));
361
1

            
362
1
			Self::deposit_event(Event::ForeignAssetXcmLocationChanged {
363
1
				asset_id,
364
1
				new_xcm_location,
365
1
			});
366
1
			Ok(())
367
		}
368

            
369
		/// Freeze a given foreign assetId
370
		#[pallet::call_index(2)]
371
		#[pallet::weight(<T as Config>::WeightInfo::freeze_foreign_asset())]
372
		pub fn freeze_foreign_asset(
373
			origin: OriginFor<T>,
374
			asset_id: AssetId,
375
			allow_xcm_deposit: bool,
376
3
		) -> DispatchResult {
377
3
			T::ForeignAssetFreezerOrigin::ensure_origin(origin)?;
378

            
379
3
			let xcm_location =
380
3
				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
381

            
382
3
			let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
383
3
				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
384

            
385
3
			ensure!(
386
3
				asset_status == AssetStatus::Active,
387
1
				Error::<T>::AssetAlreadyFrozen
388
			);
389

            
390
2
			EvmCaller::<T>::erc20_pause(asset_id)?;
391

            
392
2
			let new_asset_status = if allow_xcm_deposit {
393
2
				AssetStatus::FrozenXcmDepositAllowed
394
			} else {
395
				AssetStatus::FrozenXcmDepositForbidden
396
			};
397

            
398
2
			AssetsByLocation::<T>::insert(&xcm_location, (asset_id, new_asset_status));
399
2

            
400
2
			Self::deposit_event(Event::ForeignAssetFrozen {
401
2
				asset_id,
402
2
				xcm_location,
403
2
			});
404
2
			Ok(())
405
		}
406

            
407
		/// Unfreeze a given foreign assetId
408
		#[pallet::call_index(3)]
409
		#[pallet::weight(<T as Config>::WeightInfo::unfreeze_foreign_asset())]
410
3
		pub fn unfreeze_foreign_asset(origin: OriginFor<T>, asset_id: AssetId) -> DispatchResult {
411
3
			T::ForeignAssetUnfreezerOrigin::ensure_origin(origin)?;
412

            
413
3
			let xcm_location =
414
3
				AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
415

            
416
3
			let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
417
3
				.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;
418

            
419
3
			ensure!(
420
3
				asset_status == AssetStatus::FrozenXcmDepositAllowed
421
1
					|| asset_status == AssetStatus::FrozenXcmDepositForbidden,
422
1
				Error::<T>::AssetNotFrozen
423
			);
424

            
425
2
			EvmCaller::<T>::erc20_unpause(asset_id)?;
426

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

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

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

            
445
			if let AssetStatus::FrozenXcmDepositForbidden = asset_status {
446
				return Err(MatchError::AssetNotHandled.into());
447
			}
448

            
449
			let beneficiary = T::XcmLocationToH160::convert_location(who)
450
				.ok_or(MatchError::AccountIdConversionFailed)?;
451

            
452
			// We perform the evm transfers in a storage transaction to ensure that if it fail
453
			// any contract storage changes are rolled back.
454
			frame_support::storage::with_storage_layer(|| {
455
				EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount)
456
			})?;
457

            
458
			Ok(())
459
		}
460

            
461
		fn internal_transfer_asset(
462
			asset: &Asset,
463
			from: &Location,
464
			to: &Location,
465
			_context: &XcmContext,
466
		) -> Result<AssetsInHolding, XcmError> {
467
			let (contract_address, amount, asset_status) =
468
				ForeignAssetsMatcher::<T>::match_asset(asset)?;
469

            
470
			if let AssetStatus::FrozenXcmDepositForbidden | AssetStatus::FrozenXcmDepositAllowed =
471
				asset_status
472
			{
473
				return Err(MatchError::AssetNotHandled.into());
474
			}
475

            
476
			let from = T::XcmLocationToH160::convert_location(from)
477
				.ok_or(MatchError::AccountIdConversionFailed)?;
478

            
479
			let to = T::XcmLocationToH160::convert_location(to)
480
				.ok_or(MatchError::AccountIdConversionFailed)?;
481

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

            
488
			Ok(asset.clone().into())
489
		}
490

            
491
		// Since we don't control the erc20 contract that manages the asset we want to withdraw,
492
		// we can't really withdraw this asset, we can only transfer it to another account.
493
		// It would be possible to transfer the asset to a dedicated account that would reflect
494
		// the content of the xcm holding, but this would imply to perform two evm calls instead of
495
		// one (1 to withdraw the asset and a second one to deposit it).
496
		// In order to perform only one evm call, we just trace the origin of the asset,
497
		// and then the transfer will only really be performed in the deposit instruction.
498
18
		fn withdraw_asset(
499
18
			what: &Asset,
500
18
			who: &Location,
501
18
			_context: Option<&XcmContext>,
502
18
		) -> Result<AssetsInHolding, XcmError> {
503
18
			let (contract_address, amount, asset_status) =
504
18
				ForeignAssetsMatcher::<T>::match_asset(what)?;
505
18
			let who = T::XcmLocationToH160::convert_location(who)
506
18
				.ok_or(MatchError::AccountIdConversionFailed)?;
507

            
508
18
			if let AssetStatus::FrozenXcmDepositForbidden | AssetStatus::FrozenXcmDepositAllowed =
509
18
				asset_status
510
			{
511
				return Err(MatchError::AssetNotHandled.into());
512
18
			}
513
18

            
514
18
			// We perform the evm transfers in a storage transaction to ensure that if it fail
515
18
			// any contract storage changes are rolled back.
516
18
			frame_support::storage::with_storage_layer(|| {
517
18
				EvmCaller::<T>::erc20_burn_from(contract_address, who, amount)
518
18
			})?;
519

            
520
18
			Ok(what.clone().into())
521
18
		}
522
	}
523

            
524
	impl<T: Config> sp_runtime::traits::MaybeEquivalence<Location, AssetId> for Pallet<T> {
525
		fn convert(location: &Location) -> Option<AssetId> {
526
			AssetsByLocation::<T>::get(location).map(|(asset_id, _)| asset_id)
527
		}
528
3
		fn convert_back(asset_id: &AssetId) -> Option<Location> {
529
3
			AssetsById::<T>::get(asset_id)
530
3
		}
531
	}
532
}