git.delta.rocks / unique-network / refs/commits / 98dd35a1f09a

difftreelog

source

pallets/foreign-assets/src/lib.rs18.1 KiBsourcehistory
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Foreign Assets18//!19//! ## Overview20//!21//! The Foreign Assets is a proxy that maps XCM operations to the Unique Network's pallets logic.2223#![cfg_attr(not(feature = "std"), no_std)]24#![allow(clippy::unused_unit)]2526use core::ops::Deref;2728use frame_support::{dispatch::DispatchResult, pallet_prelude::*, traits::EnsureOrigin, PalletId};29use frame_system::pallet_prelude::*;30use pallet_common::{31	dispatch::CollectionDispatch, erc::CrossAccountId, XcmExtensions, NATIVE_FUNGIBLE_COLLECTION_ID,32};33use sp_runtime::traits::AccountIdConversion;34use sp_std::{boxed::Box, vec, vec::Vec};35use staging_xcm::{36	opaque::latest::{prelude::XcmError, Weight},37	v3::{prelude::*, MultiAsset, XcmContext},38	VersionedAssetId,39};40use staging_xcm_executor::{41	traits::{ConvertLocation, Error as XcmExecutorError, TransactAsset, WeightTrader},42	Assets,43};44use up_data_structs::{45	budget::ZeroBudget, CollectionId, CollectionMode, CollectionName, CollectionTokenPrefix,46	CreateCollectionData, CreateFungibleData, CreateItemData, TokenId,47};4849pub mod weights;5051#[cfg(feature = "runtime-benchmarks")]52mod benchmarking;5354pub use module::*;55pub use weights::WeightInfo;5657#[frame_support::pallet]58pub mod module {59	use pallet_common::CollectionIssuer;60	use up_data_structs::CollectionDescription;6162	use super::*;6364	#[pallet::config]65	pub trait Config:66		frame_system::Config67		+ pallet_common::Config68		+ pallet_fungible::Config69		+ pallet_balances::Config70	{71		/// The overarching event type.72		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;7374		/// Origin for force registering of a foreign asset.75		type ForceRegisterOrigin: EnsureOrigin<Self::RuntimeOrigin>;7677		/// The ID of the foreign assets pallet.78		type PalletId: Get<PalletId>;7980		/// Self-location of this parachain.81		type SelfLocation: Get<MultiLocation>;8283		/// The converter from a MultiLocation to a CrossAccountId.84		type LocationToAccountId: ConvertLocation<Self::CrossAccountId>;8586		/// Weight information for the extrinsics in this module.87		type WeightInfo: WeightInfo;88	}8990	#[pallet::error]91	pub enum Error<T> {92		/// The foreign asset is already registered.93		ForeignAssetAlreadyRegistered,9495		/// The given asset ID could not be converted into the current XCM version.96		BadForeignAssetId,97	}9899	#[pallet::event]100	#[pallet::generate_deposit(fn deposit_event)]101	pub enum Event<T: Config> {102		/// The foreign asset registered.103		ForeignAssetRegistered {104			collection_id: CollectionId,105			asset_id: Box<VersionedAssetId>,106		},107	}108109	/// The corresponding collections of foreign assets.110	#[pallet::storage]111	#[pallet::getter(fn foreign_asset_to_collection)]112	pub type ForeignAssetToCollection<T: Config> =113		StorageMap<_, Twox64Concat, staging_xcm::v3::AssetId, CollectionId, OptionQuery>;114115	/// The corresponding foreign assets of collections.116	#[pallet::storage]117	#[pallet::getter(fn collection_to_foreign_asset)]118	pub type CollectionToForeignAsset<T: Config> =119		StorageMap<_, Twox64Concat, CollectionId, staging_xcm::v3::AssetId, OptionQuery>;120121	/// The correponding NFT token id of reserve NFTs122	#[pallet::storage]123	#[pallet::getter(fn foreign_reserve_asset_instance_to_token_id)]124	pub type ForeignReserveAssetInstanceToTokenId<T: Config> = StorageDoubleMap<125		Hasher1 = Twox64Concat,126		Key1 = CollectionId,127		Hasher2 = Blake2_128Concat,128		Key2 = staging_xcm::v3::AssetInstance,129		Value = TokenId,130		QueryKind = OptionQuery,131	>;132133	/// The correponding reserve NFT of a token ID134	#[pallet::storage]135	#[pallet::getter(fn token_id_to_foreign_reserve_asset_instance)]136	pub type TokenIdToForeignReserveAssetInstance<T: Config> = StorageDoubleMap<137		Hasher1 = Twox64Concat,138		Key1 = CollectionId,139		Hasher2 = Blake2_128Concat,140		Key2 = TokenId,141		Value = staging_xcm::v3::AssetInstance,142		QueryKind = OptionQuery,143	>;144145	#[pallet::pallet]146	pub struct Pallet<T>(_);147148	#[pallet::call]149	impl<T: Config> Pallet<T> {150		#[pallet::call_index(0)]151		#[pallet::weight(<T as Config>::WeightInfo::force_register_foreign_asset())]152		pub fn force_register_foreign_asset(153			origin: OriginFor<T>,154			versioned_asset_id: Box<VersionedAssetId>,155			name: CollectionName,156			token_prefix: CollectionTokenPrefix,157			mode: ForeignCollectionMode,158		) -> DispatchResult {159			T::ForceRegisterOrigin::ensure_origin(origin.clone())?;160161			let asset_id: AssetId = versioned_asset_id162				.as_ref()163				.clone()164				.try_into()165				.map_err(|()| Error::<T>::BadForeignAssetId)?;166167			ensure!(168				!<ForeignAssetToCollection<T>>::contains_key(asset_id),169				<Error<T>>::ForeignAssetAlreadyRegistered,170			);171172			let foreign_collection_owner = Self::pallet_account();173174			let description: CollectionDescription = "Foreign Assets Collection"175				.encode_utf16()176				.collect::<Vec<_>>()177				.try_into()178				.expect("description length < max description length; qed");179180			let collection_id = T::CollectionDispatch::create(181				foreign_collection_owner,182				CollectionIssuer::Internals,183				CreateCollectionData {184					name,185					token_prefix,186					description,187					mode: mode.into(),188					..Default::default()189				},190			)?;191192			<ForeignAssetToCollection<T>>::insert(asset_id, collection_id);193			<CollectionToForeignAsset<T>>::insert(collection_id, asset_id);194195			Self::deposit_event(Event::<T>::ForeignAssetRegistered {196				collection_id,197				asset_id: versioned_asset_id,198			});199200			Ok(())201		}202	}203}204205impl<T: Config> Pallet<T> {206	fn pallet_account() -> T::CrossAccountId {207		let owner: T::AccountId = T::PalletId::get().into_account_truncating();208		T::CrossAccountId::from_sub(owner)209	}210211	/// Converts a concrete asset ID (the asset multilocation) to a local collection on Unique Network.212	///213	/// The multilocation corresponds to a local collection if:214	/// * It is `Here` location that corresponds to the native token of this parachain.215	/// * It is `../Parachain(<Unique Network Para ID>)` that also corresponds to the native token of this parachain.216	/// * It is `../Parachain(<Unique Network Para ID>)/GeneralIndex(<Collection ID>)` that corresponds217	/// to the collection with the ID equal to `<Collection ID>`. The `<Collection ID>` must be in the valid range,218	/// otherwise `None` is returned.219	/// * It is `GeneralIndex(<Collection ID>)`. Same as the last one above.220	///221	/// If the multilocation doesn't match the patterns listed above,222	/// or the `<Collection ID>` points to a foreign collection,223	/// `None` is returned, identifying that the given multilocation doesn't correspond to a local collection.224	fn local_asset_id_to_collection(asset_id: &AssetId) -> Option<CollectionLocality> {225		let AssetId::Concrete(asset_location) = asset_id else {226			return None;227		};228229		let self_location = T::SelfLocation::get();230231		if *asset_location == Here.into() || *asset_location == self_location {232			return Some(CollectionLocality::Local(NATIVE_FUNGIBLE_COLLECTION_ID));233		}234235		let prefix = if asset_location.parents == 0 {236			&Here237		} else if asset_location.parents == self_location.parents {238			&self_location.interior239		} else {240			return None;241		};242243		let GeneralIndex(collection_id) = asset_location.interior.match_and_split(prefix)? else {244			return None;245		};246247		let collection_id = CollectionId((*collection_id).try_into().ok()?);248249		Self::collection_to_foreign_asset(collection_id)250			.is_none()251			.then_some(CollectionLocality::Local(collection_id))252	}253254	/// Converts an asset ID to a Unique Network's collection locality (either foreign or a local one).255	///256	/// The function will check if the asset's reserve location has the corresponding257	/// foreign collection on Unique Network,258	/// and will return the "foreign" locality containing the collection ID if found.259	///260	/// If no corresponding foreign collection is found, the function will check261	/// if the asset's reserve location corresponds to a local collection.262	/// If the local collection is found, the "local" locality with the collection ID is returned.263	///264	/// If all of the above have failed, the `AssetIdConversionFailed` error will be returned.265	fn asset_to_collection(asset_id: &AssetId) -> Result<CollectionLocality, XcmError> {266		Self::foreign_asset_to_collection(asset_id)267			.map(CollectionLocality::Foreign)268			.or_else(|| Self::local_asset_id_to_collection(asset_id))269			.ok_or_else(|| XcmExecutorError::AssetIdConversionFailed.into())270	}271272	/// Converts an XCM asset instance of local collection to the Unique Network's token ID.273	///274	/// The asset instance corresponds to the Unique Network's token ID if it is in the following format:275	/// `AssetInstance::Index(<token ID>)`.276	///277	/// If the asset instance is not in the valid format or the `<token ID>` can't fit into the valid token ID,278	/// `None` will be returned.279	///280	/// Note: this function can return `Some` containing the token ID of a non-existing NFT.281	/// It returns `None` when it failed to convert the `asset_instance` to a local ID.282	fn local_asset_instance_to_token_id(asset_instance: &AssetInstance) -> Option<TokenId> {283		match asset_instance {284			AssetInstance::Index(token_id) => Some(TokenId((*token_id).try_into().ok()?)),285			_ => None,286		}287	}288289	/// Obtains the token ID of the `asset_instance` in the collection.290	///291	/// Note: this function can return `Some` containing the token ID of a non-existing NFT.292	/// It returns `None` when it failed to convert the `asset_instance` to a local ID.293	fn asset_instance_to_token_id(294		collection_locality: CollectionLocality,295		asset_instance: &AssetInstance,296	) -> Option<TokenId> {297		match collection_locality {298			CollectionLocality::Local(_) => Self::local_asset_instance_to_token_id(asset_instance),299			CollectionLocality::Foreign(collection_id) => {300				Self::foreign_reserve_asset_instance_to_token_id(collection_id, asset_instance)301			}302		}303	}304305	/// Creates a foreign item in the the collection.306	fn create_foreign_asset_instance(307		xcm_ext: &dyn XcmExtensions<T>,308		collection_id: CollectionId,309		asset_instance: &AssetInstance,310		to: T::CrossAccountId,311	) -> DispatchResult {312		let derivative_token_id = xcm_ext.create_item(313			&Self::pallet_account(),314			to,315			CreateItemData::NFT(Default::default()),316			&ZeroBudget,317		)?;318319		<ForeignReserveAssetInstanceToTokenId<T>>::insert(320			collection_id,321			asset_instance,322			derivative_token_id,323		);324325		<TokenIdToForeignReserveAssetInstance<T>>::insert(326			collection_id,327			derivative_token_id,328			asset_instance,329		);330331		Ok(())332	}333334	/// Deposits an asset instance to the `to` account.335	///336	/// Either transfers an existing item from the pallet's account337	/// or creates a foreign item.338	fn deposit_asset_instance(339		xcm_ext: &dyn XcmExtensions<T>,340		collection_locality: CollectionLocality,341		asset_instance: &AssetInstance,342		to: T::CrossAccountId,343	) -> XcmResult {344		let token_id = Self::asset_instance_to_token_id(collection_locality, asset_instance);345346		let deposit_result = match (collection_locality, token_id) {347			(_, Some(token_id)) => {348				let depositor = &Self::pallet_account();349				let from = depositor;350				let amount = 1;351352				xcm_ext.transfer_item(depositor, from, &to, token_id, amount, &ZeroBudget)353			}354			(CollectionLocality::Foreign(collection_id), None) => {355				Self::create_foreign_asset_instance(xcm_ext, collection_id, asset_instance, to)356			}357			(CollectionLocality::Local(_), None) => {358				return Err(XcmExecutorError::InstanceConversionFailed.into());359			}360		};361362		deposit_result363			.map_err(|_| XcmError::FailedToTransactAsset("non-fungible item deposit failed"))364	}365366	/// Withdraws an asset instance from the `from` account.367	///368	/// Transfers the asset instance to the pallet's account.369	fn withdraw_asset_instance(370		xcm_ext: &dyn XcmExtensions<T>,371		collection_locality: CollectionLocality,372		asset_instance: &AssetInstance,373		from: T::CrossAccountId,374	) -> XcmResult {375		let token_id = Self::asset_instance_to_token_id(collection_locality, asset_instance)376			.ok_or(XcmExecutorError::InstanceConversionFailed)?;377378		let depositor = &from;379		let to = Self::pallet_account();380		let amount = 1;381		xcm_ext382			.transfer_item(depositor, &from, &to, token_id, amount, &ZeroBudget)383			.map_err(|_| XcmError::FailedToTransactAsset("non-fungible item withdraw failed"))?;384385		Ok(())386	}387}388389impl<T: Config> TransactAsset for Pallet<T> {390	fn can_check_in(391		_origin: &MultiLocation,392		_what: &MultiAsset,393		_context: &XcmContext,394	) -> XcmResult {395		Err(XcmError::Unimplemented)396	}397398	fn check_in(_origin: &MultiLocation, _what: &MultiAsset, _context: &XcmContext) {}399400	fn can_check_out(401		_dest: &MultiLocation,402		_what: &MultiAsset,403		_context: &XcmContext,404	) -> XcmResult {405		Err(XcmError::Unimplemented)406	}407408	fn check_out(_dest: &MultiLocation, _what: &MultiAsset, _context: &XcmContext) {}409410	fn deposit_asset(411		what: &MultiAsset,412		to: &MultiLocation,413		_context: Option<&XcmContext>,414	) -> XcmResult {415		let to = T::LocationToAccountId::convert_location(to)416			.ok_or(XcmExecutorError::AccountIdConversionFailed)?;417418		let collection_locality = Self::asset_to_collection(&what.id)?;419		let dispatch = T::CollectionDispatch::dispatch(*collection_locality)420			.map_err(|_| XcmExecutorError::AssetIdConversionFailed)?;421422		let collection = dispatch.as_dyn();423		let xcm_ext = collection.xcm_extensions().ok_or(XcmError::Unimplemented)?;424425		match what.fun {426			Fungibility::Fungible(amount) => xcm_ext427				.create_item(428					&Self::pallet_account(),429					to,430					CreateItemData::Fungible(CreateFungibleData { value: amount }),431					&ZeroBudget,432				)433				.map(|_| ())434				.map_err(|_| XcmError::FailedToTransactAsset("fungible item deposit failed")),435436			Fungibility::NonFungible(asset_instance) => {437				Self::deposit_asset_instance(xcm_ext, collection_locality, &asset_instance, to)438			}439		}440	}441442	fn withdraw_asset(443		what: &MultiAsset,444		from: &MultiLocation,445		_maybe_context: Option<&XcmContext>,446	) -> Result<staging_xcm_executor::Assets, XcmError> {447		let from = T::LocationToAccountId::convert_location(from)448			.ok_or(XcmExecutorError::AccountIdConversionFailed)?;449450		let collection_locality = Self::asset_to_collection(&what.id)?;451		let dispatch = T::CollectionDispatch::dispatch(*collection_locality)452			.map_err(|_| XcmExecutorError::AssetIdConversionFailed)?;453454		let collection = dispatch.as_dyn();455		let xcm_ext = collection.xcm_extensions().ok_or(XcmError::NoPermission)?;456457		match what.fun {458			Fungibility::Fungible(amount) => xcm_ext459				.burn_item(from, TokenId::default(), amount)460				.map_err(|_| XcmError::FailedToTransactAsset("fungible item withdraw failed"))?,461462			Fungibility::NonFungible(asset_instance) => {463				Self::withdraw_asset_instance(xcm_ext, collection_locality, &asset_instance, from)?;464			}465		}466467		Ok(what.clone().into())468	}469470	fn internal_transfer_asset(471		what: &MultiAsset,472		from: &MultiLocation,473		to: &MultiLocation,474		_context: &XcmContext,475	) -> Result<staging_xcm_executor::Assets, XcmError> {476		let from = T::LocationToAccountId::convert_location(from)477			.ok_or(XcmExecutorError::AccountIdConversionFailed)?;478479		let to = T::LocationToAccountId::convert_location(to)480			.ok_or(XcmExecutorError::AccountIdConversionFailed)?;481482		let collection_locality = Self::asset_to_collection(&what.id)?;483484		let dispatch = T::CollectionDispatch::dispatch(*collection_locality)485			.map_err(|_| XcmExecutorError::AssetIdConversionFailed)?;486		let collection = dispatch.as_dyn();487		let xcm_ext = collection.xcm_extensions().ok_or(XcmError::NoPermission)?;488489		let depositor = &from;490491		let token_id;492		let amount;493		let map_error: fn(DispatchError) -> XcmError;494495		match what.fun {496			Fungibility::Fungible(fungible_amount) => {497				token_id = TokenId::default();498				amount = fungible_amount;499				map_error = |_| XcmError::FailedToTransactAsset("fungible item transfer failed");500			}501502			Fungibility::NonFungible(asset_instance) => {503				token_id = Self::asset_instance_to_token_id(collection_locality, &asset_instance)504					.ok_or(XcmExecutorError::InstanceConversionFailed)?;505506				amount = 1;507				map_error = |_| XcmError::FailedToTransactAsset("non-fungible item transfer failed")508			}509		}510511		xcm_ext512			.transfer_item(depositor, &from, &to, token_id, amount, &ZeroBudget)513			.map_err(map_error)?;514515		Ok(what.clone().into())516	}517}518519#[derive(Clone, Copy)]520pub enum CollectionLocality {521	Local(CollectionId),522	Foreign(CollectionId),523}524525impl Deref for CollectionLocality {526	type Target = CollectionId;527528	fn deref(&self) -> &Self::Target {529		match self {530			Self::Local(id) => id,531			Self::Foreign(id) => id,532		}533	}534}535536pub struct CurrencyIdConvert<T: Config>(PhantomData<T>);537impl<T: Config> sp_runtime::traits::Convert<CollectionId, Option<MultiLocation>>538	for CurrencyIdConvert<T>539{540	fn convert(collection_id: CollectionId) -> Option<MultiLocation> {541		if collection_id == NATIVE_FUNGIBLE_COLLECTION_ID {542			Some(T::SelfLocation::get())543		} else {544			<Pallet<T>>::collection_to_foreign_asset(collection_id)545				.and_then(|asset_id| match asset_id {546					AssetId::Concrete(location) => Some(location),547					_ => None,548				})549				.or_else(|| {550					T::SelfLocation::get()551						.pushed_with_interior(GeneralIndex(collection_id.0.into()))552						.ok()553				})554		}555	}556}557558#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq, TypeInfo, MaxEncodedLen)]559pub enum ForeignCollectionMode {560	NFT,561	Fungible(u8),562}563564impl From<ForeignCollectionMode> for CollectionMode {565	fn from(value: ForeignCollectionMode) -> Self {566		match value {567			ForeignCollectionMode::NFT => Self::NFT,568			ForeignCollectionMode::Fungible(decimals) => Self::Fungible(decimals),569		}570	}571}572573pub struct FreeForAll;574575impl WeightTrader for FreeForAll {576	fn new() -> Self {577		Self578	}579580	fn buy_weight(581		&mut self,582		weight: Weight,583		payment: Assets,584		_xcm: &XcmContext,585	) -> Result<Assets, XcmError> {586		log::trace!(target: "fassets::weight", "buy_weight weight: {:?}, payment: {:?}", weight, payment);587		Ok(payment)588	}589}