1234567891011121314151617181920212223#![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};39use staging_xcm_executor::{40 traits::{ConvertLocation, Error as XcmExecutorError, TransactAsset, WeightTrader},41 Assets,42};43use up_data_structs::{44 budget::ZeroBudget, CollectionId, CollectionMode, CollectionName, CollectionTokenPrefix,45 CreateCollectionData, CreateFungibleData, CreateItemData, CreateNftData, Property, PropertyKey,46 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::{61 CollectionDescription, Property, PropertyKeyPermission, PropertyPermission,62 };6364 use super::*;6566 #[pallet::config]67 pub trait Config:68 frame_system::Config69 + pallet_common::Config70 + pallet_fungible::Config71 + pallet_balances::Config72 {73 74 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;7576 77 type ForceRegisterOrigin: EnsureOrigin<Self::RuntimeOrigin>;7879 80 type PalletId: Get<PalletId>;8182 83 type SelfLocation: Get<MultiLocation>;8485 86 type LocationToAccountId: ConvertLocation<Self::CrossAccountId>;8788 89 type WeightInfo: WeightInfo;90 }9192 #[pallet::error]93 pub enum Error<T> {94 95 ForeignAssetAlreadyRegistered,96 }9798 #[pallet::event]99 #[pallet::generate_deposit(fn deposit_event)]100 pub enum Event<T: Config> {101 102 ForeignAssetRegistered {103 collection_id: CollectionId,104 asset_id: Box<AssetId>,105 },106 }107108 109 #[pallet::storage]110 #[pallet::getter(fn foreign_asset_to_collection)]111 pub type ForeignAssetToCollection<T: Config> =112 StorageMap<_, Twox64Concat, staging_xcm::v3::AssetId, CollectionId, OptionQuery>;113114 115 #[pallet::storage]116 #[pallet::getter(fn collection_to_foreign_asset)]117 pub type CollectionToForeignAsset<T: Config> =118 StorageMap<_, Twox64Concat, CollectionId, staging_xcm::v3::AssetId, OptionQuery>;119120 121 #[pallet::storage]122 #[pallet::getter(fn foreign_reserve_asset_instance_to_token_id)]123 pub type ForeignReserveAssetInstanceToTokenId<T: Config> = StorageDoubleMap<124 Hasher1 = Twox64Concat,125 Key1 = CollectionId,126 Hasher2 = Blake2_128Concat,127 Key2 = staging_xcm::v3::AssetInstance,128 Value = TokenId,129 QueryKind = OptionQuery,130 >;131132 133 #[pallet::storage]134 #[pallet::getter(fn token_id_to_foreign_reserve_asset_instance)]135 pub type TokenIdToForeignReserveAssetInstance<T: Config> = StorageDoubleMap<136 Hasher1 = Twox64Concat,137 Key1 = CollectionId,138 Hasher2 = Blake2_128Concat,139 Key2 = TokenId,140 Value = staging_xcm::v3::AssetInstance,141 QueryKind = OptionQuery,142 >;143144 #[pallet::pallet]145 pub struct Pallet<T>(_);146147 #[pallet::call]148 impl<T: Config> Pallet<T> {149 #[pallet::call_index(0)]150 #[pallet::weight(<T as Config>::WeightInfo::register_foreign_asset())]151 pub fn force_register_foreign_asset(152 origin: OriginFor<T>,153 asset_id: Box<AssetId>,154 name: CollectionName,155 token_prefix: CollectionTokenPrefix,156 mode: ForeignCollectionMode,157 ) -> DispatchResult {158 T::ForceRegisterOrigin::ensure_origin(origin.clone())?;159160 ensure!(161 !<ForeignAssetToCollection<T>>::contains_key(*asset_id),162 <Error<T>>::ForeignAssetAlreadyRegistered,163 );164165 let foreign_collection_owner = Self::pallet_account();166167 let description: CollectionDescription = "Foreign Assets Collection"168 .encode_utf16()169 .collect::<Vec<_>>()170 .try_into()171 .expect("description length < max description length; qed");172173 let collection_id = T::CollectionDispatch::create(174 foreign_collection_owner,175 CollectionIssuer::Internals,176 CreateCollectionData {177 name,178 token_prefix,179 description,180 mode: mode.into(),181182 properties: vec![Property {183 key: Self::reserve_location_property_key(),184 value: asset_id185 .encode()186 .try_into()187 .expect("multilocation is less than 32k; qed"),188 }]189 .try_into()190 .expect("just one property can always be stored; qed"),191192 token_property_permissions: vec![PropertyKeyPermission {193 key: Self::reserve_asset_instance_property_key(),194 permission: PropertyPermission {195 mutable: false,196 collection_admin: true,197 token_owner: false,198 },199 }]200 .try_into()201 .expect("just one property permission can always be stored; qed"),202 ..Default::default()203 },204 )?;205206 <ForeignAssetToCollection<T>>::insert(*asset_id, collection_id);207 <CollectionToForeignAsset<T>>::insert(collection_id, *asset_id);208209 Self::deposit_event(Event::<T>::ForeignAssetRegistered {210 collection_id,211 asset_id,212 });213214 Ok(())215 }216 }217}218219impl<T: Config> Pallet<T> {220 fn pallet_account() -> T::CrossAccountId {221 let owner: T::AccountId = T::PalletId::get().into_account_truncating();222 T::CrossAccountId::from_sub(owner)223 }224225 fn reserve_location_property_key() -> PropertyKey {226 b"reserve-location"227 .to_vec()228 .try_into()229 .expect("key length < max property key length; qed")230 }231232 fn reserve_asset_instance_property_key() -> PropertyKey {233 b"reserve-asset-instance"234 .to_vec()235 .try_into()236 .expect("key length < max property key length; qed")237 }238239 240 241 242 243 244 245 246 247 248 249 250 251 252 fn local_asset_id_to_collection(asset_id: &AssetId) -> Option<CollectionLocality> {253 let AssetId::Concrete(asset_location) = asset_id else {254 return None;255 };256257 let self_location = T::SelfLocation::get();258259 if *asset_location == Here.into() || *asset_location == self_location {260 return Some(CollectionLocality::Local(NATIVE_FUNGIBLE_COLLECTION_ID));261 }262263 let prefix = if asset_location.parents == 0 {264 &Here265 } else if asset_location.parents == self_location.parents {266 &self_location.interior267 } else {268 return None;269 };270271 let GeneralIndex(collection_id) = asset_location.interior.match_and_split(prefix)? else {272 return None;273 };274275 let collection_id = CollectionId((*collection_id).try_into().ok()?);276277 Self::collection_to_foreign_asset(collection_id)278 .is_none()279 .then_some(CollectionLocality::Local(collection_id))280 }281282 283 284 285 286 287 288 289 290 291 292 293 fn asset_to_collection(asset_id: &AssetId) -> Result<CollectionLocality, XcmError> {294 Self::foreign_asset_to_collection(asset_id)295 .map(CollectionLocality::Foreign)296 .or_else(|| Self::local_asset_id_to_collection(asset_id))297 .ok_or_else(|| XcmExecutorError::AssetIdConversionFailed.into())298 }299300 301 302 303 304 305 306 307 fn local_asset_instance_to_token_id(asset_instance: &AssetInstance) -> Option<TokenId> {308 match asset_instance {309 AssetInstance::Index(token_id) => Some(TokenId((*token_id).try_into().ok()?)),310 _ => None,311 }312 }313314 315 fn asset_instance_to_token_id(316 collection_locality: CollectionLocality,317 asset_instance: &AssetInstance,318 ) -> Option<TokenId> {319 match collection_locality {320 CollectionLocality::Local(_) => Self::local_asset_instance_to_token_id(asset_instance),321 CollectionLocality::Foreign(collection_id) => {322 Self::foreign_reserve_asset_instance_to_token_id(collection_id, asset_instance)323 }324 }325 }326327 328 fn create_foreign_asset_instance(329 xcm_ext: &dyn XcmExtensions<T>,330 collection_id: CollectionId,331 asset_instance: &AssetInstance,332 to: T::CrossAccountId,333 ) -> DispatchResult {334 let asset_instance_encoded = asset_instance.encode();335336 let derivative_token_id = xcm_ext.create_item(337 &Self::pallet_account(),338 to,339 CreateItemData::NFT(CreateNftData {340 properties: vec![Property {341 key: Self::reserve_asset_instance_property_key(),342 value: asset_instance_encoded343 .try_into()344 .expect("asset instance length <= 32 bytes which is less than value length limit; qed"),345 }]346 .try_into()347 .expect("just one property can always be stored; qed"),348 }),349 &ZeroBudget,350 )?;351352 <ForeignReserveAssetInstanceToTokenId<T>>::insert(353 collection_id,354 asset_instance,355 derivative_token_id,356 );357358 <TokenIdToForeignReserveAssetInstance<T>>::insert(359 collection_id,360 derivative_token_id,361 asset_instance,362 );363364 Ok(())365 }366367 368 369 370 371 fn deposit_asset_instance(372 xcm_ext: &dyn XcmExtensions<T>,373 collection_locality: CollectionLocality,374 asset_instance: &AssetInstance,375 to: T::CrossAccountId,376 ) -> XcmResult {377 let token_id = Self::asset_instance_to_token_id(collection_locality, asset_instance);378379 let deposit_result = match (collection_locality, token_id) {380 (_, Some(token_id)) => {381 let depositor = &Self::pallet_account();382 let from = depositor;383 let amount = 1;384385 xcm_ext.transfer_item(depositor, from, &to, token_id, amount, &ZeroBudget)386 }387 (CollectionLocality::Foreign(collection_id), None) => {388 Self::create_foreign_asset_instance(xcm_ext, collection_id, asset_instance, to)389 }390 (CollectionLocality::Local(_), None) => {391 return Err(XcmError::AssetNotFound);392 }393 };394395 deposit_result396 .map_err(|_| XcmError::FailedToTransactAsset("non-fungible item deposit failed"))397 }398399 400 401 402 fn withdraw_asset_instance(403 xcm_ext: &dyn XcmExtensions<T>,404 collection_locality: CollectionLocality,405 asset_instance: &AssetInstance,406 from: T::CrossAccountId,407 ) -> XcmResult {408 let token_id = Self::asset_instance_to_token_id(collection_locality, asset_instance)409 .ok_or(XcmError::AssetNotFound)?;410411 let depositor = &from;412 let to = Self::pallet_account();413 let amount = 1;414 xcm_ext415 .transfer_item(depositor, &from, &to, token_id, amount, &ZeroBudget)416 .map_err(|_| XcmError::FailedToTransactAsset("non-fungible item withdraw failed"))?;417418 Ok(())419 }420}421422impl<T: Config> TransactAsset for Pallet<T> {423 fn can_check_in(424 _origin: &MultiLocation,425 _what: &MultiAsset,426 _context: &XcmContext,427 ) -> XcmResult {428 Err(XcmError::Unimplemented)429 }430431 fn check_in(_origin: &MultiLocation, _what: &MultiAsset, _context: &XcmContext) {}432433 fn can_check_out(434 _dest: &MultiLocation,435 _what: &MultiAsset,436 _context: &XcmContext,437 ) -> XcmResult {438 Err(XcmError::Unimplemented)439 }440441 fn check_out(_dest: &MultiLocation, _what: &MultiAsset, _context: &XcmContext) {}442443 fn deposit_asset(444 what: &MultiAsset,445 to: &MultiLocation,446 _context: Option<&XcmContext>,447 ) -> XcmResult {448 let to = T::LocationToAccountId::convert_location(to)449 .ok_or(XcmExecutorError::AccountIdConversionFailed)?;450451 let collection_locality = Self::asset_to_collection(&what.id)?;452 let dispatch = T::CollectionDispatch::dispatch(*collection_locality)453 .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_locality, &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_locality = Self::asset_to_collection(&what.id)?;484 let dispatch = T::CollectionDispatch::dispatch(*collection_locality)485 .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_locality, &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_locality = Self::asset_to_collection(&what.id)?;516517 let dispatch = T::CollectionDispatch::dispatch(*collection_locality)518 .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_locality, &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}551552#[derive(Clone, Copy)]553pub enum CollectionLocality {554 Local(CollectionId),555 Foreign(CollectionId),556}557558impl Deref for CollectionLocality {559 type Target = CollectionId;560561 fn deref(&self) -> &Self::Target {562 match self {563 Self::Local(id) => id,564 Self::Foreign(id) => id,565 }566 }567}568569pub struct CurrencyIdConvert<T: Config>(PhantomData<T>);570impl<T: Config> sp_runtime::traits::Convert<CollectionId, Option<MultiLocation>>571 for CurrencyIdConvert<T>572{573 fn convert(collection_id: CollectionId) -> Option<MultiLocation> {574 if collection_id == NATIVE_FUNGIBLE_COLLECTION_ID {575 Some(T::SelfLocation::get())576 } else {577 <Pallet<T>>::collection_to_foreign_asset(collection_id)578 .and_then(|asset_id| match asset_id {579 AssetId::Concrete(location) => Some(location),580 _ => None,581 })582 .or_else(|| {583 T::SelfLocation::get()584 .pushed_with_interior(GeneralIndex(collection_id.0.into()))585 .ok()586 })587 }588 }589}590591#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq, TypeInfo, MaxEncodedLen)]592pub enum ForeignCollectionMode {593 NFT,594 Fungible(u8),595}596597impl From<ForeignCollectionMode> for CollectionMode {598 fn from(value: ForeignCollectionMode) -> Self {599 match value {600 ForeignCollectionMode::NFT => Self::NFT,601 ForeignCollectionMode::Fungible(decimals) => Self::Fungible(decimals),602 }603 }604}605606pub struct FreeForAll;607608impl WeightTrader for FreeForAll {609 fn new() -> Self {610 Self611 }612613 fn buy_weight(614 &mut self,615 weight: Weight,616 payment: Assets,617 _xcm: &XcmContext,618 ) -> Result<Assets, XcmError> {619 log::trace!(target: "fassets::weight", "buy_weight weight: {:?}, payment: {:?}", weight, payment);620 Ok(payment)621 }622}