Skip to content

Commit c1dbcb7

Browse files
mrshiposhabkonturfranciscoaguirre
authored
XCM NFT types that use Granular NFT traits (#4300)
## Overview This PR provides new XCM types and tools for building NFT Asset Transactors. The new types use general and granular NFT traits from #5620. The new XCM adapters and utility types to work with NFTs can be considered the main deliverable of the **[XCM NFT proposal](https://polkadot.polkassembly.io/referenda/379)**. The new types use a more general approach, making integration into any chain with various NFT implementations easier. For instance, different implementations could use: * different ID assignment approaches * predefined NFT IDs - pallet-uniques, pallet-nfts * derived NFT IDs (NFT IDs are automatically derived from collection IDs) - Unique Network, ORML/Acala, Aventus * classless (collection-less) tokens - CoreTime NFTs, Aventus NFT Manager NFTs * in-class (in-collection) tokens - Unique Network, pallet-uniques, pallet-nfts, ORML/Acala * different approaches to storing associated data on-chain: * data is stored entirely separately from tokens - pallet-uniques * data is stored partially or entirely within tokens (i.e., burning a token means destroying it with its data) - pallet-nfts ([partially](https://github.com/paritytech/polkadot-sdk/blob/8b4cfda7589325d1a34f70b3770ab494a9d4052c/substrate/frame/nfts/src/features/create_delete_item.rs#L240-L241)), Unique Network, ORML/Acala With new types, these differences can be abstracted away. Moreover, the new types provide greater flexibility for supporting derivative NFTs, allowing several possible approaches depending on the given chain's team's goals or restrictions (see the `pallet-derivatives` crate docs and mock docs). Also, this is the PR I mentioned in the #4073 issue, as it can be viewed as the solution. In particular, the new adapter (`UniqueInstancesAdapter`) requires the `Update` operation with the `ChangeOwnerFrom` strategy. This brings the attention of both a developer and a reviewer to the `ChangeOwnerFrom` strategy (meaning that the transfer checks if the asset can be transferred from a given account to another account), both at trait bound and at the call site, without sacrificing the flexibility of the NFT traits. ## New types for xcm-builder and xcm-executor This PR introduces several XCM types. The `UniqueInstancesAdapter` is a new `TransactAsset` adapter that supersedes both `NonFungibleAdapter` and `NonFungiblesAdapter` (for reserve-based transfers only, as teleports can't be implemented appropriately without transferring the NFT metadata alongside it; no standard solution exists for that yet. Hopefully, the Felloweship RFC 125 ([PR](polkadot-fellows/RFCs#125), [text](https://github.com/polkadot-fellows/RFCs/blob/3a24444278f22dac414fbd2b5c4b205f73d78af7/text/0125-xcm-asset-metadata.md)) will help with that). Thanks to the new Matcher types, the new adapter can be used instead of both `NonFungibleAdapter` and `NonFungiblesAdapter`: * `MatchesInstance` (a trait) * `MatchInClassInstances` * `MatchClasslessInstances` The `UniqueInstancesAdapter` works with existing tokens only. To create new tokens (derivative ones), there is the `UniqueInstancesDepositAdapter`. See the `pallet-derivatives` mock and tests for the usage example. ### Superseding the old adapters for pallet-uniques Here is how the new `UniqueInstancesAdapter` in Westend Asset Hub replaces the `NonFungiblesAdapter`: ```rust /// Means for transacting unique assets. pub type UniquesTransactor = UniqueInstancesAdapter< AccountId, LocationToAccountId, MatchInClassInstances<UniquesConvertedConcreteId>, pallet_uniques::asset_ops::Item<Uniques>, >; ``` `MatchInClassInstances` allows us to reuse the already existing `UniquesConvertedConcreteId` matcher. The `pallet_uniques::asset_ops::Item<Uniques>` already implements the needed traits. So, migrating from the old adapter to the new one regarding runtime config changes is easy. >NOTE: `pallet_uniques::asset_ops::Item` grants access to the asset operations of NFT items of a given pallet-uniques instance, whereas `pallet_uniques::asset_ops::Collection` grants access to the collection operations. ### Declarative modification of an NFT engine If an NFT-hosting pallet only implements a transfer operation but not the `Stash` and `Restore`, one could declaratively add them using the `UniqueInstancesWithStashAccount` adapter. So, you can use it with the `UniqueInstancesAdapter` as follows: ```rust parameter_types! { pub StashAccountId: AccountId = /* Some Stash Account ID */; } type Transactor = UniqueInstancesAdapter< AccountId, LocationToAccountId, Matcher, UniqueInstancesWithStashAccount<StashAccountId, NftEngine>, >; ``` ### Supporting derivative NFTs (reserve-based model) There are several possible scenarios of supporting derivative NFTs (and their collections, if applicable). A separate NFT-hosting pallet instance could be configured to use XCM `AssetId` as collection ID and `AssetInstance` token ID. In that case, the asset transaction doesn't need any special treatment. However, registering NFT collections might require a special API. Also, if the NFT-hosting pallet can't be configured to use XCM ID types, we would need to store a mapping between the XCM ID of the original token and the derivative ID. See the `pallet-derivatives` crate docs for details. The example of its usage can be found in its mock and tests. #### TODO No benchmarks were run for `pallet-derivatives`, so there is no `weights.rs` for it yet. --------- Co-authored-by: Branislav Kontur <bkontur@gmail.com> Co-authored-by: Francisco Aguirre <franciscoaguirreperez@gmail.com>
1 parent 8468c3e commit c1dbcb7

30 files changed

Lines changed: 2867 additions & 55 deletions

File tree

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ members = [
356356
"substrate/frame/core-fellowship",
357357
"substrate/frame/delegated-staking",
358358
"substrate/frame/democracy",
359+
"substrate/frame/derivatives",
359360
"substrate/frame/dummy-dim",
360361
"substrate/frame/election-provider-multi-block",
361362
"substrate/frame/election-provider-multi-phase",
@@ -1051,6 +1052,7 @@ pallet-staking-runtime-api = { path = "substrate/frame/staking/runtime-api", def
10511052
revive-dev-node = { path = "substrate/frame/revive/dev-node/node" }
10521053
revive-dev-runtime = { path = "substrate/frame/revive/dev-node/runtime" }
10531054
# TODO: remove the reward stuff as they are not needed here
1055+
pallet-derivatives = { path = "polkadot/xcm/pallet-derivatives", default-features = false }
10541056
pallet-staking-async = { path = "substrate/frame/staking-async", default-features = false }
10551057
pallet-staking-async-ah-client = { path = "substrate/frame/staking-async/ah-client", default-features = false }
10561058
pallet-staking-async-parachain-runtime = { path = "substrate/frame/staking-async/runtimes/parachain" }

cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,14 @@ use testnet_parachains_constants::rococo::snowbridge::{
4848
};
4949
use xcm::latest::{prelude::*, ROCOCO_GENESIS_HASH, WESTEND_GENESIS_HASH};
5050
use xcm_builder::{
51-
AccountId32Aliases, AliasChildLocation, AllowExplicitUnpaidExecutionFrom,
52-
AllowHrmpNotificationsFromRelayChain, AllowKnownQueryResponses, AllowSubscriptionsFrom,
53-
AllowTopLevelPaidExecutionFrom, DenyRecursively, DenyReserveTransferToRelayChain, DenyThenTry,
54-
DescribeAllTerminal, DescribeFamily, EnsureXcmOrigin, ExternalConsensusLocationsConverterFor,
51+
unique_instances::UniqueInstancesAdapter, AccountId32Aliases, AliasChildLocation,
52+
AllowExplicitUnpaidExecutionFrom, AllowHrmpNotificationsFromRelayChain,
53+
AllowKnownQueryResponses, AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom,
54+
DenyRecursively, DenyReserveTransferToRelayChain, DenyThenTry, DescribeAllTerminal,
55+
DescribeFamily, EnsureXcmOrigin, ExternalConsensusLocationsConverterFor,
5556
FrameTransactionalProcessor, FungibleAdapter, FungiblesAdapter, HashedDescription, IsConcrete,
56-
LocalMint, MatchedConvertedConcreteId, NetworkExportTableItem, NoChecking, NonFungiblesAdapter,
57-
ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, SendXcmFeeToAccount,
57+
LocalMint, MatchInClassInstances, MatchedConvertedConcreteId, NetworkExportTableItem,
58+
NoChecking, ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, SendXcmFeeToAccount,
5859
SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative,
5960
SignedToAccountId32, SingleAssetExchangeAdapter, SovereignPaidRemoteExporter,
6061
SovereignSignedViaLocation, StartsWith, StartsWithExplicitGlobalConsensus, TakeWeightCredit,
@@ -147,19 +148,11 @@ pub type UniquesConvertedConcreteId =
147148
assets_common::UniquesConvertedConcreteId<UniquesPalletLocation>;
148149

149150
/// Means for transacting unique assets.
150-
pub type UniquesTransactor = NonFungiblesAdapter<
151-
// Use this non-fungibles implementation:
152-
Uniques,
153-
// This adapter will handle any non-fungible asset from the uniques pallet.
154-
UniquesConvertedConcreteId,
155-
// Convert an XCM Location into a local account id:
156-
LocationToAccountId,
157-
// Our chain's account ID type (we can't get away without mentioning it explicitly):
151+
pub type UniquesTransactor = UniqueInstancesAdapter<
158152
AccountId,
159-
// Does not check teleports.
160-
NoChecking,
161-
// The account to use for tracking teleports.
162-
CheckingAccount,
153+
LocationToAccountId,
154+
MatchInClassInstances<UniquesConvertedConcreteId>,
155+
pallet_uniques::asset_ops::Item<Uniques>,
163156
>;
164157

165158
/// `AssetId`/`Balance` converter for `ForeignAssets`.

cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,14 @@ use westend_runtime_constants::{
4848
};
4949
use xcm::latest::{prelude::*, ROCOCO_GENESIS_HASH, WESTEND_GENESIS_HASH};
5050
use xcm_builder::{
51-
AccountId32Aliases, AliasChildLocation, AllowExplicitUnpaidExecutionFrom,
52-
AllowHrmpNotificationsFromRelayChain, AllowKnownQueryResponses, AllowSubscriptionsFrom,
53-
AllowTopLevelPaidExecutionFrom, DenyRecursively, DenyReserveTransferToRelayChain, DenyThenTry,
54-
DescribeAllTerminal, DescribeFamily, EnsureXcmOrigin, ExternalConsensusLocationsConverterFor,
51+
unique_instances::UniqueInstancesAdapter, AccountId32Aliases, AliasChildLocation,
52+
AllowExplicitUnpaidExecutionFrom, AllowHrmpNotificationsFromRelayChain,
53+
AllowKnownQueryResponses, AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom,
54+
DenyRecursively, DenyReserveTransferToRelayChain, DenyThenTry, DescribeAllTerminal,
55+
DescribeFamily, EnsureXcmOrigin, ExternalConsensusLocationsConverterFor,
5556
FrameTransactionalProcessor, FungibleAdapter, FungiblesAdapter, HashedDescription, IsConcrete,
56-
LocalMint, MatchedConvertedConcreteId, MintLocation, NetworkExportTableItem, NoChecking,
57-
NonFungiblesAdapter, OriginToPluralityVoice, ParentAsSuperuser, ParentIsPreset,
57+
LocalMint, MatchInClassInstances, MatchedConvertedConcreteId, MintLocation,
58+
NetworkExportTableItem, NoChecking, OriginToPluralityVoice, ParentAsSuperuser, ParentIsPreset,
5859
RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, SiblingParachainConvertsVia,
5960
SignedAccountId32AsNative, SignedToAccountId32, SingleAssetExchangeAdapter,
6061
SovereignSignedViaLocation, StartsWith, StartsWithExplicitGlobalConsensus, TakeWeightCredit,
@@ -143,19 +144,11 @@ pub type UniquesConvertedConcreteId =
143144
assets_common::UniquesConvertedConcreteId<UniquesPalletLocation>;
144145

145146
/// Means for transacting unique assets.
146-
pub type UniquesTransactor = NonFungiblesAdapter<
147-
// Use this non-fungibles implementation:
148-
Uniques,
149-
// This adapter will handle any non-fungible asset from the uniques pallet.
150-
UniquesConvertedConcreteId,
151-
// Convert an XCM Location into a local account id:
152-
LocationToAccountId,
153-
// Our chain's account ID type (we can't get away without mentioning it explicitly):
147+
pub type UniquesTransactor = UniqueInstancesAdapter<
154148
AccountId,
155-
// Does not check teleports.
156-
NoChecking,
157-
// The account to use for tracking teleports.
158-
CheckingAccount,
149+
LocationToAccountId,
150+
MatchInClassInstances<UniquesConvertedConcreteId>,
151+
pallet_uniques::asset_ops::Item<Uniques>,
159152
>;
160153

161154
/// `AssetId`/`Balance` converter for `ForeignAssets`.

cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs

Lines changed: 174 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ use asset_hub_westend_runtime::{
2525
governance, xcm_config,
2626
xcm_config::{
2727
bridging, CheckingAccount, LocationToAccountId, StakingPot,
28-
TrustBackedAssetsPalletLocation, WestendLocation, XcmConfig,
28+
TrustBackedAssetsPalletLocation, UniquesConvertedConcreteId, UniquesPalletLocation,
29+
WestendLocation, XcmConfig,
2930
},
3031
AllPalletsWithoutSystem, Assets, Balances, Block, ExistentialDeposit, ForeignAssets,
3132
ForeignAssetsInstance, MetadataDepositBase, MetadataDepositPerByte, ParachainSystem,
3233
PolkadotXcm, Revive, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, SessionKeys,
33-
ToRococoXcmRouterInstance, TrustBackedAssetsInstance, XcmpQueue,
34+
ToRococoXcmRouterInstance, TrustBackedAssetsInstance, Uniques, XcmpQueue,
3435
};
3536
pub use asset_hub_westend_runtime::{AssetConversion, AssetDeposit, CollatorSelection, System};
3637
use asset_test_utils::{
@@ -45,6 +46,10 @@ use frame_support::{
4546
fungibles::{
4647
self, Create, Inspect as FungiblesInspect, InspectEnumerable, Mutate as FungiblesMutate,
4748
},
49+
tokens::asset_ops::{
50+
common_strategies::{Bytes, Owner},
51+
Inspect as InspectUniqueAsset,
52+
},
4853
ContainsPair,
4954
},
5055
weights::{Weight, WeightToFee as WeightToFeeT},
@@ -55,10 +60,11 @@ use pallet_revive::{
5560
Code, DepositLimit,
5661
};
5762
use pallet_revive_fixtures::compile_module;
63+
use pallet_uniques::{asset_ops::Item, asset_strategies::Attribute};
5864
use parachains_common::{AccountId, AssetIdForTrustBackedAssets, AuraId, Balance};
5965
use sp_consensus_aura::SlotDuration;
6066
use sp_core::crypto::Ss58Codec;
61-
use sp_runtime::{traits::MaybeEquivalence, Either};
67+
use sp_runtime::{traits::MaybeEquivalence, Either, MultiAddress};
6268
use std::convert::Into;
6369
use testnet_parachains_constants::westend::{consensus::*, currency::UNITS, fee::WeightToFee};
6470
use xcm::{
@@ -68,8 +74,11 @@ use xcm::{
6874
},
6975
VersionedXcm,
7076
};
71-
use xcm_builder::WithLatestLocationConverter;
72-
use xcm_executor::traits::{ConvertLocation, JustTry, WeightTrader};
77+
use xcm_builder::{
78+
unique_instances::UniqueInstancesAdapter as NewNftAdapter, MatchInClassInstances, NoChecking,
79+
NonFungiblesAdapter as OldNftAdapter, WithLatestLocationConverter,
80+
};
81+
use xcm_executor::traits::{ConvertLocation, JustTry, TransactAsset, WeightTrader};
7382
use xcm_runtime_apis::conversions::LocationToAccountHelper;
7483

7584
const ALICE: [u8; 32] = [1u8; 32];
@@ -503,6 +512,166 @@ fn test_asset_xcm_take_first_trader_not_possible_for_non_sufficient_assets() {
503512
});
504513
}
505514

515+
fn test_nft_asset_transactor_works<T: TransactAsset>() {
516+
ExtBuilder::<Runtime>::default()
517+
.with_tracing()
518+
.with_collators(vec![AccountId::from(ALICE)])
519+
.with_session_keys(vec![(
520+
AccountId::from(ALICE),
521+
AccountId::from(ALICE),
522+
SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) },
523+
)])
524+
.build()
525+
.execute_with(|| {
526+
let collection_id = 42;
527+
let item_id = 101;
528+
529+
let alice = AccountId::from(ALICE);
530+
let bob = AccountId::from(BOB);
531+
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
532+
533+
assert_ok!(Balances::mint_into(&alice, 2 * UNITS));
534+
535+
assert_ok!(Uniques::create(
536+
RuntimeHelper::origin_of(alice.clone()),
537+
collection_id,
538+
MultiAddress::Id(alice.clone()),
539+
));
540+
541+
assert_ok!(Uniques::mint(
542+
RuntimeHelper::origin_of(alice.clone()),
543+
collection_id,
544+
item_id,
545+
MultiAddress::Id(bob.clone()),
546+
));
547+
548+
let attr_key = vec![0xA, 0xA, 0xB, 0xB];
549+
let attr_value = vec![0xC, 0x0, 0x0, 0x1, 0xF, 0x0, 0x0, 0xD];
550+
551+
assert_ok!(Uniques::set_attribute(
552+
RuntimeHelper::origin_of(alice.clone()),
553+
collection_id,
554+
Some(item_id),
555+
attr_key.clone().try_into().unwrap(),
556+
attr_value.clone().try_into().unwrap(),
557+
));
558+
559+
let collection_location = UniquesPalletLocation::get()
560+
.appended_with(GeneralIndex(collection_id.into()))
561+
.unwrap();
562+
let item_asset: Asset =
563+
(collection_location, AssetInstance::Index(item_id.into())).into();
564+
565+
let alice_account_location: Location = alice.clone().into();
566+
let bob_account_location: Location = bob.clone().into();
567+
568+
// Can't deposit the token that isn't withdrawn
569+
assert_err!(
570+
T::deposit_asset(&item_asset, &alice_account_location, Some(&ctx),),
571+
XcmError::FailedToTransactAsset("AlreadyExists")
572+
);
573+
574+
// Alice isn't the owner, she can't withdraw the token
575+
assert_noop!(
576+
T::withdraw_asset(&item_asset, &alice_account_location, Some(&ctx),),
577+
XcmError::FailedToTransactAsset("NoPermission")
578+
);
579+
580+
// Bob, the owner, can withdraw the token
581+
assert_ok!(T::withdraw_asset(&item_asset, &bob_account_location, Some(&ctx),));
582+
583+
// The token is withdrawn
584+
assert_eq!(
585+
Item::<Uniques>::inspect(&(collection_id, item_id), Owner::default()),
586+
Err(pallet_uniques::Error::<Runtime>::UnknownItem.into()),
587+
);
588+
589+
// But the attribute data is preserved as the pallet-uniques works that way.
590+
assert_eq!(
591+
Item::<Uniques>::inspect(
592+
&(collection_id, item_id),
593+
Bytes(Attribute(attr_key.as_slice()))
594+
),
595+
Ok(attr_value.clone()),
596+
);
597+
598+
// Can't withdraw the already withdrawn token
599+
assert_err!(
600+
T::withdraw_asset(&item_asset, &bob_account_location, Some(&ctx),),
601+
XcmError::FailedToTransactAsset("UnknownCollection")
602+
);
603+
604+
// Deposit the token to alice
605+
assert_ok!(T::deposit_asset(&item_asset, &alice_account_location, Some(&ctx),));
606+
607+
// The token is deposited
608+
assert_eq!(
609+
Item::<Uniques>::inspect(&(collection_id, item_id), Owner::default()),
610+
Ok(alice.clone()),
611+
);
612+
613+
// The attribute data is the same
614+
assert_eq!(
615+
Item::<Uniques>::inspect(
616+
&(collection_id, item_id),
617+
Bytes(Attribute(attr_key.as_slice()))
618+
),
619+
Ok(attr_value.clone()),
620+
);
621+
622+
// Can't deposit the token twice
623+
assert_err!(
624+
T::deposit_asset(&item_asset, &alice_account_location, Some(&ctx),),
625+
XcmError::FailedToTransactAsset("AlreadyExists")
626+
);
627+
628+
// Transfer the token directly
629+
assert_ok!(T::transfer_asset(
630+
&item_asset,
631+
&alice_account_location,
632+
&bob_account_location,
633+
&ctx,
634+
));
635+
636+
// The token's owner has changed
637+
assert_eq!(
638+
Item::<Uniques>::inspect(&(collection_id, item_id), Owner::default()),
639+
Ok(bob.clone()),
640+
);
641+
642+
// The attribute data is the same
643+
assert_eq!(
644+
Item::<Uniques>::inspect(
645+
&(collection_id, item_id),
646+
Bytes(Attribute(attr_key.as_slice()))
647+
),
648+
Ok(attr_value.clone()),
649+
);
650+
});
651+
}
652+
653+
#[test]
654+
fn test_new_nft_config_works_as_the_old_one() {
655+
type OldNftTransactor = OldNftAdapter<
656+
Uniques,
657+
UniquesConvertedConcreteId,
658+
LocationToAccountId,
659+
AccountId,
660+
NoChecking,
661+
CheckingAccount,
662+
>;
663+
664+
type NewNftTransactor = NewNftAdapter<
665+
AccountId,
666+
LocationToAccountId,
667+
MatchInClassInstances<UniquesConvertedConcreteId>,
668+
Item<Uniques>,
669+
>;
670+
671+
test_nft_asset_transactor_works::<OldNftTransactor>();
672+
test_nft_asset_transactor_works::<NewNftTransactor>();
673+
}
674+
506675
#[test]
507676
fn test_assets_balances_api_works() {
508677
use assets_common::runtime_api::runtime_decl_for_fungibles_api::FungiblesApi;

0 commit comments

Comments
 (0)