git.delta.rocks / unique-network / refs/commits / 2840b3d211f1

difftreelog

source

pallets/foreign-assets/src/lib.rs18.7 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 frame_support::{dispatch::DispatchResult, pallet_prelude::*, traits::EnsureOrigin, PalletId};27use frame_system::pallet_prelude::*;28use pallet_common::{29	dispatch::CollectionDispatch, erc::CrossAccountId, XcmExtensions, NATIVE_FUNGIBLE_COLLECTION_ID,30};31use sp_runtime::traits::AccountIdConversion;32use sp_std::{boxed::Box, vec, vec::Vec};33use staging_xcm::{34	opaque::latest::{prelude::XcmError, Weight},35	v3::{prelude::*, MultiAsset, XcmContext},36};37use staging_xcm_executor::{38	traits::{ConvertLocation, Error as XcmExecutorError, TransactAsset, WeightTrader},39	Assets,40};41use up_data_structs::{42	budget::ZeroBudget, CollectionId, CollectionMode, CollectionName, CollectionTokenPrefix,43	CreateCollectionData, CreateFungibleData, CreateItemData, CreateNftData, Property, PropertyKey,44	TokenId,45};4647pub mod weights;4849#[cfg(feature = "runtime-benchmarks")]50mod benchmarking;5152pub use module::*;53pub use weights::WeightInfo;5455#[frame_support::pallet]56pub mod module {57	use up_data_structs::{58		CollectionDescription, Property, PropertyKeyPermission, PropertyPermission,59	};6061	use super::*;6263	#[pallet::config]64	pub trait Config:65		frame_system::Config66		+ pallet_common::Config67		+ pallet_fungible::Config68		+ pallet_balances::Config69	{70		/// The overarching event type.71		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;7273		/// Origin for force registering of a foreign asset.74		type ForceRegisterOrigin: EnsureOrigin<Self::RuntimeOrigin>;7576		/// The ID of the foreign assets pallet.77		type PalletId: Get<PalletId>;7879		/// Self-location of this parachain.80		type SelfLocation: Get<MultiLocation>;8182		/// The converter from a MultiLocation to a CrossAccountId.83		type LocationToAccountId: ConvertLocation<Self::CrossAccountId>;8485		/// Weight information for the extrinsics in this module.86		type WeightInfo: WeightInfo;87	}8889	#[pallet::error]90	pub enum Error<T> {91		/// The foreign asset is already registered.92		ForeignAssetAlreadyRegistered,93	}9495	#[pallet::event]96	#[pallet::generate_deposit(fn deposit_event)]97	pub enum Event<T: Config> {98		/// The foreign asset registered.99		ForeignAssetRegistered {100			asset_id: CollectionId,101			reserve_location: Box<MultiLocation>,102		},103	}104105	/// The corresponding collections of reserve locations.106	#[pallet::storage]107	#[pallet::getter(fn foreign_reserve_location_to_collection)]108	pub type ForeignReserveLocationToCollection<T: Config> =109		StorageMap<_, Twox64Concat, staging_xcm::v3::MultiLocation, CollectionId, OptionQuery>;110111	/// The corresponding reserve location of collections.112	#[pallet::storage]113	#[pallet::getter(fn collection_to_foreign_reserve_location)]114	pub type CollectionToForeignReserveLocation<T: Config> =115		StorageMap<_, Twox64Concat, CollectionId, staging_xcm::v3::MultiLocation, OptionQuery>;116117	/// The correponding NFT token id of reserve NFTs118	#[pallet::storage]119	#[pallet::getter(fn foreign_reserve_asset_instance_to_token_id)]120	pub type ForeignReserveAssetInstanceToTokenId<T: Config> = StorageDoubleMap<121		Hasher1 = Twox64Concat,122		Key1 = CollectionId,123		Hasher2 = Twox64Concat,124		Key2 = staging_xcm::v3::AssetInstance,125		Value = TokenId,126		QueryKind = OptionQuery,127	>;128129	/// The correponding reserve NFT of a token ID130	#[pallet::storage]131	#[pallet::getter(fn token_id_to_foreign_reserve_asset_instance)]132	pub type TokenIdToForeignReserveAssetInstance<T: Config> = StorageDoubleMap<133		Hasher1 = Twox64Concat,134		Key1 = CollectionId,135		Hasher2 = Twox64Concat,136		Key2 = TokenId,137		Value = staging_xcm::v3::AssetInstance,138		QueryKind = OptionQuery,139	>;140141	#[pallet::pallet]142	pub struct Pallet<T>(_);143144	#[pallet::call]145	impl<T: Config> Pallet<T> {146		#[pallet::call_index(0)]147		#[pallet::weight(<T as Config>::WeightInfo::register_foreign_asset())]148		pub fn force_register_foreign_asset(149			origin: OriginFor<T>,150			reserve_location: Box<MultiLocation>,151			name: CollectionName,152			token_prefix: CollectionTokenPrefix,153			mode: ForeignCollectionMode,154		) -> DispatchResult {155			T::ForceRegisterOrigin::ensure_origin(origin.clone())?;156157			if <ForeignReserveLocationToCollection<T>>::contains_key(*reserve_location) {158				return Err(<Error<T>>::ForeignAssetAlreadyRegistered.into());159			}160161			let foreign_collection_owner = Self::pallet_account();162163			let description: CollectionDescription = "Foreign Assets Collection"164				.encode_utf16()165				.collect::<Vec<_>>()166				.try_into()167				.expect("description length < max description length; qed");168169			let payer = None;170			let is_special_collection = true;171			let collection_id = T::CollectionDispatch::create_raw(172				foreign_collection_owner,173				payer,174				is_special_collection,175				CreateCollectionData {176					name,177					token_prefix,178					description,179					mode: mode.into(),180181					properties: vec![Property {182						key: Self::reserve_location_property_key(),183						value: reserve_location184							.encode()185							.try_into()186							.expect("multilocation is less than 32k; qed"),187					}]188					.try_into()189					.expect("just one property can always be stored; qed"),190191					token_property_permissions: vec![PropertyKeyPermission {192						key: Self::reserve_asset_instance_property_key(),193						permission: PropertyPermission {194							mutable: false,195							collection_admin: true,196							token_owner: false,197						},198					}]199					.try_into()200					.expect("just one property permission can always be stored; qed"),201					..Default::default()202				},203			)?;204205			<ForeignReserveLocationToCollection<T>>::insert(*reserve_location, collection_id);206			<CollectionToForeignReserveLocation<T>>::insert(collection_id, *reserve_location);207208			Self::deposit_event(Event::<T>::ForeignAssetRegistered {209				asset_id: collection_id,210				reserve_location,211			});212213			Ok(())214		}215	}216}217218impl<T: Config> Pallet<T> {219	fn pallet_account() -> T::CrossAccountId {220		let owner: T::AccountId = T::PalletId::get().into_account_truncating();221		T::CrossAccountId::from_sub(owner)222	}223224	fn reserve_location_property_key() -> PropertyKey {225		b"reserve-location"226			.to_vec()227			.try_into()228			.expect("key length < max property key length; qed")229	}230231	fn reserve_asset_instance_property_key() -> PropertyKey {232		b"reserve-asset-instance"233			.to_vec()234			.try_into()235			.expect("key length < max property key length; qed")236	}237238	/// Converts a multilocation to a local collection on Unique Network.239	///240	/// The multilocation corresponds to a local collection if:241	/// * It is `Here` location that corresponds to the native token of this parachain.242	/// * It is `../Parachain(<Unique Network Para ID>)` that also corresponds to the native token of this parachain.243	/// * It is `../Parachain(<Unique Network Para ID>)/GeneralIndex(<Collection ID>)` that corresponds244	/// to the collection with the ID equal to `<Collection ID>`. The `<Collection ID>` must be in the valid range,245	/// otherwise `None` is returned.246	///247	/// If the multilocation doesn't match the patterns listed above,248	/// or the `<Collection ID>` points to a foreign collection,249	/// `None` is returned, identifying that the given multilocation doesn't correspond to a local collection.250	fn local_asset_location_to_collection(asset_location: &MultiLocation) -> Option<CollectionId> {251		let self_location = T::SelfLocation::get();252253		if *asset_location == Here.into() || *asset_location == self_location {254			Some(NATIVE_FUNGIBLE_COLLECTION_ID)255		} else if asset_location.parents == self_location.parents {256			match asset_location257				.interior258				.match_and_split(&self_location.interior)259			{260				Some(GeneralIndex(collection_id)) => {261					let collection_id = CollectionId((*collection_id).try_into().ok()?);262263					Self::collection_to_foreign_reserve_location(collection_id)264						.is_none()265						.then_some(collection_id)266				}267				_ => None,268			}269		} else {270			None271		}272	}273274	/// Converts an asset ID to a Unique Network's collection (either foreign or a local one).275	///276	/// The function will check if the asset's reserve location has the corresponding277	/// foreign collection on Unique Network, and will return the collection ID if found.278	///279	/// If no corresponding foreign collection is found, the function will check280	/// if the asset's reserve location corresponds to a local collection.281	/// If the local collection is found, its ID is returned.282	///283	/// If all of the above have failed, the `AssetIdConversionFailed` error will be returned.284	fn asset_to_collection(asset_id: &AssetId) -> Result<CollectionId, XcmError> {285		let AssetId::Concrete(asset_reserve_location) = asset_id else {286			return Err(XcmExecutorError::AssetNotHandled.into());287		};288289		Self::foreign_reserve_location_to_collection(asset_reserve_location)290			.or_else(|| Self::local_asset_location_to_collection(asset_reserve_location))291			.ok_or_else(|| XcmExecutorError::AssetIdConversionFailed.into())292	}293294	/// Converts an XCM asset instance of local collection to the Unique Network's token ID.295	///296	/// The asset instance corresponds to the Unique Network's token ID if it is in the following format:297	/// `AssetInstance::Index(<token ID>)`.298	///299	/// If the asset instance is not in the valid format or the `<token ID>` can't fit into the valid token ID,300	/// the `AssetNotFound` error will be returned.301	fn local_asset_instance_to_token_id(302		asset_instance: &AssetInstance,303	) -> Result<TokenId, XcmError> {304		match asset_instance {305			AssetInstance::Index(token_id) => Ok(TokenId(306				(*token_id)307					.try_into()308					.map_err(|_| XcmError::AssetNotFound)?,309			)),310			_ => Err(XcmError::AssetNotFound),311		}312	}313314	/// Obtains the token ID of the `asset_instance` in the collection.315	///316	/// Returns `Ok(None)` only if the `asset_instance` is a part of a foreign collection317	/// and the item hasn't yet been created on this blockchain.318	///319	/// If the `asset_instance` exists and it is a part of a foreign collection, the function will return `Ok(Some(<token ID>))`.320	///321	/// If the `asset_instance` is a part of a local collection,322	/// the function will return either `Ok(Some(<token ID>))` or an error if the token is not found.323	fn asset_instance_to_token_id(324		collection_id: CollectionId,325		asset_instance: &AssetInstance,326	) -> Result<Option<TokenId>, XcmError> {327		if <CollectionToForeignReserveLocation<T>>::contains_key(collection_id) {328			Ok(Self::foreign_reserve_asset_instance_to_token_id(329				collection_id,330				asset_instance,331			))332		} else {333			Self::local_asset_instance_to_token_id(asset_instance).map(Some)334		}335	}336337	/// Creates a foreign item in the the collection.338	fn create_foreign_asset_instance(339		xcm_ext: &dyn XcmExtensions<T>,340		collection_id: CollectionId,341		asset_instance: &AssetInstance,342		to: T::CrossAccountId,343	) -> DispatchResult {344		let asset_instance_encoded = asset_instance.encode();345346		let derivative_token_id = xcm_ext.create_item(347			&Self::pallet_account(),348			to,349			CreateItemData::NFT(CreateNftData {350				properties: vec![Property {351					key: Self::reserve_asset_instance_property_key(),352					value: asset_instance_encoded353						.try_into()354						.expect("asset instance length <= 32 bytes which is less than value length limit; qed"),355				}]356				.try_into()357				.expect("just one property can always be stored; qed"),358			}),359			&ZeroBudget,360		)?;361362		<ForeignReserveAssetInstanceToTokenId<T>>::insert(363			collection_id,364			asset_instance,365			derivative_token_id,366		);367368		<TokenIdToForeignReserveAssetInstance<T>>::insert(369			collection_id,370			derivative_token_id,371			asset_instance,372		);373374		Ok(())375	}376377	/// Deposits an asset instance to the `to` account.378	///379	/// Either transfers an existing item from the pallet's account380	/// or creates a foreign item.381	fn deposit_asset_instance(382		xcm_ext: &dyn XcmExtensions<T>,383		collection_id: CollectionId,384		asset_instance: &AssetInstance,385		to: T::CrossAccountId,386	) -> XcmResult {387		let deposit_result = if let Some(token_id) =388			Self::asset_instance_to_token_id(collection_id, asset_instance)?389		{390			let depositor = &Self::pallet_account();391			let from = depositor;392			let amount = 1;393394			xcm_ext.transfer_item(depositor, from, &to, token_id, amount, &ZeroBudget)395		} else {396			Self::create_foreign_asset_instance(xcm_ext, collection_id, asset_instance, to)397		};398399		deposit_result400			.map_err(|_| XcmError::FailedToTransactAsset("non-fungible item deposit failed"))401	}402403	/// Withdraws an asset instance from the `from` account.404	///405	/// Transfers the asset instance to the pallet's account.406	fn withdraw_asset_instance(407		xcm_ext: &dyn XcmExtensions<T>,408		collection_id: CollectionId,409		asset_instance: &AssetInstance,410		from: T::CrossAccountId,411	) -> XcmResult {412		let token_id = Self::asset_instance_to_token_id(collection_id, asset_instance)?413			.ok_or(XcmError::AssetNotFound)?;414415		let depositor = &from;416		let to = Self::pallet_account();417		let amount = 1;418		xcm_ext419			.transfer_item(depositor, &from, &to, token_id, amount, &ZeroBudget)420			.map_err(|_| XcmError::FailedToTransactAsset("non-fungible item withdraw failed"))?;421422		Ok(())423	}424}425426impl<T: Config> TransactAsset for Pallet<T> {427	fn can_check_in(428		_origin: &MultiLocation,429		_what: &MultiAsset,430		_context: &XcmContext,431	) -> XcmResult {432		Err(XcmError::Unimplemented)433	}434435	fn check_in(_origin: &MultiLocation, _what: &MultiAsset, _context: &XcmContext) {}436437	fn can_check_out(438		_dest: &MultiLocation,439		_what: &MultiAsset,440		_context: &XcmContext,441	) -> XcmResult {442		Err(XcmError::Unimplemented)443	}444445	fn check_out(_dest: &MultiLocation, _what: &MultiAsset, _context: &XcmContext) {}446447	fn deposit_asset(what: &MultiAsset, to: &MultiLocation, _context: &XcmContext) -> XcmResult {448		let to = T::LocationToAccountId::convert_location(to)449			.ok_or(XcmExecutorError::AccountIdConversionFailed)?;450451		let collection_id = Self::asset_to_collection(&what.id)?;452		let dispatch =453			T::CollectionDispatch::dispatch(collection_id).map_err(|_| XcmError::AssetNotFound)?;454455		let collection = dispatch.as_dyn();456		let xcm_ext = collection.xcm_extensions().ok_or(XcmError::Unimplemented)?;457458		match what.fun {459			Fungibility::Fungible(amount) => xcm_ext460				.create_item(461					&Self::pallet_account(),462					to,463					CreateItemData::Fungible(CreateFungibleData { value: amount }),464					&ZeroBudget,465				)466				.map(|_| ())467				.map_err(|_| XcmError::FailedToTransactAsset("fungible item deposit failed")),468469			Fungibility::NonFungible(asset_instance) => {470				Self::deposit_asset_instance(xcm_ext, collection_id, &asset_instance, to)471			}472		}473	}474475	fn withdraw_asset(476		what: &MultiAsset,477		from: &MultiLocation,478		_maybe_context: Option<&XcmContext>,479	) -> Result<staging_xcm_executor::Assets, XcmError> {480		let from = T::LocationToAccountId::convert_location(from)481			.ok_or(XcmExecutorError::AccountIdConversionFailed)?;482483		let collection_id = Self::asset_to_collection(&what.id)?;484		let dispatch =485			T::CollectionDispatch::dispatch(collection_id).map_err(|_| XcmError::AssetNotFound)?;486487		let collection = dispatch.as_dyn();488		let xcm_ext = collection.xcm_extensions().ok_or(XcmError::NoPermission)?;489490		match what.fun {491			Fungibility::Fungible(amount) => xcm_ext492				.burn_item(from, TokenId::default(), amount)493				.map_err(|_| XcmError::FailedToTransactAsset("fungible item withdraw failed"))?,494495			Fungibility::NonFungible(asset_instance) => {496				Self::withdraw_asset_instance(xcm_ext, collection_id, &asset_instance, from)?;497			}498		}499500		Ok(what.clone().into())501	}502503	fn internal_transfer_asset(504		what: &MultiAsset,505		from: &MultiLocation,506		to: &MultiLocation,507		_context: &XcmContext,508	) -> Result<staging_xcm_executor::Assets, XcmError> {509		let from = T::LocationToAccountId::convert_location(from)510			.ok_or(XcmExecutorError::AccountIdConversionFailed)?;511512		let to = T::LocationToAccountId::convert_location(to)513			.ok_or(XcmExecutorError::AccountIdConversionFailed)?;514515		let collection_id = Self::asset_to_collection(&what.id)?;516517		let dispatch =518			T::CollectionDispatch::dispatch(collection_id).map_err(|_| XcmError::AssetNotFound)?;519		let collection = dispatch.as_dyn();520		let xcm_ext = collection.xcm_extensions().ok_or(XcmError::NoPermission)?;521522		let depositor = &from;523524		let token_id;525		let amount;526		let map_error: fn(DispatchError) -> XcmError;527528		match what.fun {529			Fungibility::Fungible(fungible_amount) => {530				token_id = TokenId::default();531				amount = fungible_amount;532				map_error = |_| XcmError::FailedToTransactAsset("fungible item transfer failed");533			}534535			Fungibility::NonFungible(asset_instance) => {536				token_id = Self::asset_instance_to_token_id(collection_id, &asset_instance)?537					.ok_or(XcmError::AssetNotFound)?;538539				amount = 1;540				map_error = |_| XcmError::FailedToTransactAsset("non-fungible item transfer failed")541			}542		}543544		xcm_ext545			.transfer_item(depositor, &from, &to, token_id, amount, &ZeroBudget)546			.map_err(map_error)?;547548		Ok(what.clone().into())549	}550}551552pub struct CurrencyIdConvert<T: Config>(PhantomData<T>);553impl<T: Config> sp_runtime::traits::Convert<CollectionId, Option<MultiLocation>>554	for CurrencyIdConvert<T>555{556	fn convert(collection_id: CollectionId) -> Option<MultiLocation> {557		if collection_id == NATIVE_FUNGIBLE_COLLECTION_ID {558			Some(T::SelfLocation::get())559		} else {560			<Pallet<T>>::collection_to_foreign_reserve_location(collection_id).or_else(|| {561				T::SelfLocation::get()562					.pushed_with_interior(GeneralIndex(collection_id.0.into()))563					.ok()564			})565		}566	}567}568569pub use frame_support::{570	traits::{571		fungibles::Balanced, tokens::currency::Currency as CurrencyT, OnUnbalanced as OnUnbalancedT,572	},573	weights::{WeightToFee, WeightToFeePolynomial},574};575576#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq, TypeInfo, MaxEncodedLen)]577pub enum ForeignCollectionMode {578	NFT,579	Fungible(u8),580}581582impl From<ForeignCollectionMode> for CollectionMode {583	fn from(value: ForeignCollectionMode) -> Self {584		match value {585			ForeignCollectionMode::NFT => Self::NFT,586			ForeignCollectionMode::Fungible(decimals) => Self::Fungible(decimals),587		}588	}589}590591pub struct FreeForAll;592593impl WeightTrader for FreeForAll {594	fn new() -> Self {595		Self596	}597598	fn buy_weight(599		&mut self,600		weight: Weight,601		payment: Assets,602		_xcm: &XcmContext,603	) -> Result<Assets, XcmError> {604		log::trace!(target: "fassets::weight", "buy_weight weight: {:?}, payment: {:?}", weight, payment);605		Ok(payment)606	}607}