difftreelog
doc: more info about asset-instance to token id conv
in: master
1 file changed
pallets/foreign-assets/src/lib.rsdiffbeforeafterboth1// 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::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 fn local_asset_instance_to_token_id(asset_instance: &AssetInstance) -> Option<TokenId> {282 match asset_instance {283 AssetInstance::Index(token_id) => Some(TokenId((*token_id).try_into().ok()?)),284 _ => None,285 }286 }287288 /// Obtains the token ID of the `asset_instance` in the collection.289 fn asset_instance_to_token_id(290 collection_locality: CollectionLocality,291 asset_instance: &AssetInstance,292 ) -> Option<TokenId> {293 match collection_locality {294 CollectionLocality::Local(_) => Self::local_asset_instance_to_token_id(asset_instance),295 CollectionLocality::Foreign(collection_id) => {296 Self::foreign_reserve_asset_instance_to_token_id(collection_id, asset_instance)297 }298 }299 }300301 /// Creates a foreign item in the the collection.302 fn create_foreign_asset_instance(303 xcm_ext: &dyn XcmExtensions<T>,304 collection_id: CollectionId,305 asset_instance: &AssetInstance,306 to: T::CrossAccountId,307 ) -> DispatchResult {308 let derivative_token_id = xcm_ext.create_item(309 &Self::pallet_account(),310 to,311 CreateItemData::NFT(Default::default()),312 &ZeroBudget,313 )?;314315 <ForeignReserveAssetInstanceToTokenId<T>>::insert(316 collection_id,317 asset_instance,318 derivative_token_id,319 );320321 <TokenIdToForeignReserveAssetInstance<T>>::insert(322 collection_id,323 derivative_token_id,324 asset_instance,325 );326327 Ok(())328 }329330 /// Deposits an asset instance to the `to` account.331 ///332 /// Either transfers an existing item from the pallet's account333 /// or creates a foreign item.334 fn deposit_asset_instance(335 xcm_ext: &dyn XcmExtensions<T>,336 collection_locality: CollectionLocality,337 asset_instance: &AssetInstance,338 to: T::CrossAccountId,339 ) -> XcmResult {340 let token_id = Self::asset_instance_to_token_id(collection_locality, asset_instance);341342 let deposit_result = match (collection_locality, token_id) {343 (_, Some(token_id)) => {344 let depositor = &Self::pallet_account();345 let from = depositor;346 let amount = 1;347348 xcm_ext.transfer_item(depositor, from, &to, token_id, amount, &ZeroBudget)349 }350 (CollectionLocality::Foreign(collection_id), None) => {351 Self::create_foreign_asset_instance(xcm_ext, collection_id, asset_instance, to)352 }353 (CollectionLocality::Local(_), None) => {354 return Err(XcmExecutorError::InstanceConversionFailed.into());355 }356 };357358 deposit_result359 .map_err(|_| XcmError::FailedToTransactAsset("non-fungible item deposit failed"))360 }361362 /// Withdraws an asset instance from the `from` account.363 ///364 /// Transfers the asset instance to the pallet's account.365 fn withdraw_asset_instance(366 xcm_ext: &dyn XcmExtensions<T>,367 collection_locality: CollectionLocality,368 asset_instance: &AssetInstance,369 from: T::CrossAccountId,370 ) -> XcmResult {371 let token_id = Self::asset_instance_to_token_id(collection_locality, asset_instance)372 .ok_or(XcmExecutorError::InstanceConversionFailed)?;373374 let depositor = &from;375 let to = Self::pallet_account();376 let amount = 1;377 xcm_ext378 .transfer_item(depositor, &from, &to, token_id, amount, &ZeroBudget)379 .map_err(|_| XcmError::FailedToTransactAsset("non-fungible item withdraw failed"))?;380381 Ok(())382 }383}384385impl<T: Config> TransactAsset for Pallet<T> {386 fn can_check_in(387 _origin: &MultiLocation,388 _what: &MultiAsset,389 _context: &XcmContext,390 ) -> XcmResult {391 Err(XcmError::Unimplemented)392 }393394 fn check_in(_origin: &MultiLocation, _what: &MultiAsset, _context: &XcmContext) {}395396 fn can_check_out(397 _dest: &MultiLocation,398 _what: &MultiAsset,399 _context: &XcmContext,400 ) -> XcmResult {401 Err(XcmError::Unimplemented)402 }403404 fn check_out(_dest: &MultiLocation, _what: &MultiAsset, _context: &XcmContext) {}405406 fn deposit_asset(407 what: &MultiAsset,408 to: &MultiLocation,409 _context: Option<&XcmContext>,410 ) -> XcmResult {411 let to = T::LocationToAccountId::convert_location(to)412 .ok_or(XcmExecutorError::AccountIdConversionFailed)?;413414 let collection_locality = Self::asset_to_collection(&what.id)?;415 let dispatch = T::CollectionDispatch::dispatch(*collection_locality)416 .map_err(|_| XcmExecutorError::AssetIdConversionFailed)?;417418 let collection = dispatch.as_dyn();419 let xcm_ext = collection.xcm_extensions().ok_or(XcmError::Unimplemented)?;420421 match what.fun {422 Fungibility::Fungible(amount) => xcm_ext423 .create_item(424 &Self::pallet_account(),425 to,426 CreateItemData::Fungible(CreateFungibleData { value: amount }),427 &ZeroBudget,428 )429 .map(|_| ())430 .map_err(|_| XcmError::FailedToTransactAsset("fungible item deposit failed")),431432 Fungibility::NonFungible(asset_instance) => {433 Self::deposit_asset_instance(xcm_ext, collection_locality, &asset_instance, to)434 }435 }436 }437438 fn withdraw_asset(439 what: &MultiAsset,440 from: &MultiLocation,441 _maybe_context: Option<&XcmContext>,442 ) -> Result<staging_xcm_executor::Assets, XcmError> {443 let from = T::LocationToAccountId::convert_location(from)444 .ok_or(XcmExecutorError::AccountIdConversionFailed)?;445446 let collection_locality = Self::asset_to_collection(&what.id)?;447 let dispatch = T::CollectionDispatch::dispatch(*collection_locality)448 .map_err(|_| XcmExecutorError::AssetIdConversionFailed)?;449450 let collection = dispatch.as_dyn();451 let xcm_ext = collection.xcm_extensions().ok_or(XcmError::NoPermission)?;452453 match what.fun {454 Fungibility::Fungible(amount) => xcm_ext455 .burn_item(from, TokenId::default(), amount)456 .map_err(|_| XcmError::FailedToTransactAsset("fungible item withdraw failed"))?,457458 Fungibility::NonFungible(asset_instance) => {459 Self::withdraw_asset_instance(xcm_ext, collection_locality, &asset_instance, from)?;460 }461 }462463 Ok(what.clone().into())464 }465466 fn internal_transfer_asset(467 what: &MultiAsset,468 from: &MultiLocation,469 to: &MultiLocation,470 _context: &XcmContext,471 ) -> Result<staging_xcm_executor::Assets, XcmError> {472 let from = T::LocationToAccountId::convert_location(from)473 .ok_or(XcmExecutorError::AccountIdConversionFailed)?;474475 let to = T::LocationToAccountId::convert_location(to)476 .ok_or(XcmExecutorError::AccountIdConversionFailed)?;477478 let collection_locality = Self::asset_to_collection(&what.id)?;479480 let dispatch = T::CollectionDispatch::dispatch(*collection_locality)481 .map_err(|_| XcmExecutorError::AssetIdConversionFailed)?;482 let collection = dispatch.as_dyn();483 let xcm_ext = collection.xcm_extensions().ok_or(XcmError::NoPermission)?;484485 let depositor = &from;486487 let token_id;488 let amount;489 let map_error: fn(DispatchError) -> XcmError;490491 match what.fun {492 Fungibility::Fungible(fungible_amount) => {493 token_id = TokenId::default();494 amount = fungible_amount;495 map_error = |_| XcmError::FailedToTransactAsset("fungible item transfer failed");496 }497498 Fungibility::NonFungible(asset_instance) => {499 token_id = Self::asset_instance_to_token_id(collection_locality, &asset_instance)500 .ok_or(XcmExecutorError::InstanceConversionFailed)?;501502 amount = 1;503 map_error = |_| XcmError::FailedToTransactAsset("non-fungible item transfer failed")504 }505 }506507 xcm_ext508 .transfer_item(depositor, &from, &to, token_id, amount, &ZeroBudget)509 .map_err(map_error)?;510511 Ok(what.clone().into())512 }513}514515#[derive(Clone, Copy)]516pub enum CollectionLocality {517 Local(CollectionId),518 Foreign(CollectionId),519}520521impl Deref for CollectionLocality {522 type Target = CollectionId;523524 fn deref(&self) -> &Self::Target {525 match self {526 Self::Local(id) => id,527 Self::Foreign(id) => id,528 }529 }530}531532pub struct CurrencyIdConvert<T: Config>(PhantomData<T>);533impl<T: Config> sp_runtime::traits::Convert<CollectionId, Option<MultiLocation>>534 for CurrencyIdConvert<T>535{536 fn convert(collection_id: CollectionId) -> Option<MultiLocation> {537 if collection_id == NATIVE_FUNGIBLE_COLLECTION_ID {538 Some(T::SelfLocation::get())539 } else {540 <Pallet<T>>::collection_to_foreign_asset(collection_id)541 .and_then(|asset_id| match asset_id {542 AssetId::Concrete(location) => Some(location),543 _ => None,544 })545 .or_else(|| {546 T::SelfLocation::get()547 .pushed_with_interior(GeneralIndex(collection_id.0.into()))548 .ok()549 })550 }551 }552}553554#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq, TypeInfo, MaxEncodedLen)]555pub enum ForeignCollectionMode {556 NFT,557 Fungible(u8),558}559560impl From<ForeignCollectionMode> for CollectionMode {561 fn from(value: ForeignCollectionMode) -> Self {562 match value {563 ForeignCollectionMode::NFT => Self::NFT,564 ForeignCollectionMode::Fungible(decimals) => Self::Fungible(decimals),565 }566 }567}568569pub struct FreeForAll;570571impl WeightTrader for FreeForAll {572 fn new() -> Self {573 Self574 }575576 fn buy_weight(577 &mut self,578 weight: Weight,579 payment: Assets,580 _xcm: &XcmContext,581 ) -> Result<Assets, XcmError> {582 log::trace!(target: "fassets::weight", "buy_weight weight: {:?}, payment: {:?}", weight, payment);583 Ok(payment)584 }585}