git.delta.rocks / unique-network / refs/commits / b08cb26894de

difftreelog

Merge pull request #355 from UniqueNetwork/feature/nft-children

Yaroslav Bolyukin2022-05-30parents: #764dfd5 #8cbbfc6.patch.diff
in: master
Structure children map

12 files changed

modifiedpallets/common/src/lib.rsdiffbeforeafterboth
--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -353,6 +353,8 @@
 		MustBeTokenOwner,
 		/// No permission to perform action
 		NoPermission,
+		/// Destroying only empty collections is allowed
+		CantDestroyNotEmptyCollection,
 		/// Collection is not in mint mode.
 		PublicMintingNotAllowed,
 		/// Address is not in allow list.
@@ -1268,6 +1270,18 @@
 		budget: &dyn Budget,
 	) -> DispatchResult;
 
+	fn nest(
+		&self,
+		under: TokenId,
+		to_nest: (CollectionId, TokenId)
+	);
+
+	fn unnest(
+		&self,
+		under: TokenId,
+		to_nest: (CollectionId, TokenId)
+	);
+
 	fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;
 	fn collection_tokens(&self) -> Vec<TokenId>;
 	fn token_exists(&self, token: TokenId) -> bool;
modifiedpallets/fungible/src/common.rsdiffbeforeafterboth
--- a/pallets/fungible/src/common.rs
+++ b/pallets/fungible/src/common.rs
@@ -298,6 +298,18 @@
 		fail!(<Error<T>>::FungibleDisallowsNesting)
 	}
 
+	fn nest(
+		&self,
+		_under: TokenId,
+		_to_nest: (CollectionId, TokenId)
+	) {}
+
+	fn unnest(
+		&self,
+		_under: TokenId,
+		_to_nest: (CollectionId, TokenId)
+	) {}
+
 	fn collection_tokens(&self) -> Vec<TokenId> {
 		vec![TokenId::default()]
 	}
modifiedpallets/fungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/fungible/src/lib.rs
+++ b/pallets/fungible/src/lib.rs
@@ -25,8 +25,8 @@
 	budget::Budget,
 };
 use pallet_common::{
-	Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, CollectionHandle,
-	dispatch::CollectionDispatch, eth::collection_id_to_address,
+	Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,
+	eth::collection_id_to_address,
 };
 use pallet_evm::Pallet as PalletEvm;
 use pallet_structure::Pallet as PalletStructure;
@@ -145,6 +145,10 @@
 	) -> DispatchResult {
 		let id = collection.id;
 
+		if Self::collection_has_tokens(id) {
+			return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());
+		}
+
 		// =========
 
 		PalletCommon::destroy_collection(collection.0, sender)?;
@@ -155,6 +159,10 @@
 		Ok(())
 	}
 
+	fn collection_has_tokens(collection_id: CollectionId) -> bool {
+		<TotalSupply<T>>::get(collection_id) != 0
+	}
+
 	pub fn burn(
 		collection: &FungibleHandle<T>,
 		owner: &T::CrossAccountId,
@@ -176,6 +184,11 @@
 
 		if balance == 0 {
 			<Balance<T>>::remove((collection.id, owner));
+			<PalletStructure<T>>::unnest_if_nested(
+				owner,
+				collection.id,
+				TokenId::default()
+			);
 		} else {
 			<Balance<T>>::insert((collection.id, owner), balance);
 		}
@@ -229,25 +242,25 @@
 			None
 		};
 
-		if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
-			let handle = <CollectionHandle<T>>::try_get(target.0)?;
-			let dispatch = T::CollectionDispatch::dispatch(handle);
-			let dispatch = dispatch.as_dyn();
+		// =========
 
-			dispatch.check_nesting(
-				from.clone(),
-				(collection.id, TokenId::default()),
-				target.1,
-				nesting_budget,
-			)?;
-		}
+		<PalletStructure<T>>::nest_if_sent_to_token(
+			from.clone(),
+			to,
+			collection.id,
+			TokenId::default(),
+			nesting_budget
+		)?;
 
-		// =========
-
 		if let Some(balance_to) = balance_to {
 			// from != to
 			if balance_from == 0 {
 				<Balance<T>>::remove((collection.id, from));
+				<PalletStructure<T>>::unnest_if_nested(
+					from,
+					collection.id,
+					TokenId::default()
+				);
 			} else {
 				<Balance<T>>::insert((collection.id, from), balance_from);
 			}
@@ -306,18 +319,13 @@
 		}
 
 		for (to, _) in balances.iter() {
-			if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
-				let handle = <CollectionHandle<T>>::try_get(target.0)?;
-				let dispatch = T::CollectionDispatch::dispatch(handle);
-				let dispatch = dispatch.as_dyn();
-
-				dispatch.check_nesting(
-					sender.clone(),
-					(collection.id, TokenId::default()),
-					target.1,
-					nesting_budget,
-				)?;
-			}
+			<PalletStructure<T>>::check_nesting(
+				sender.clone(),
+				to,
+				collection.id,
+				TokenId::default(),
+				nesting_budget,
+			)?;
 		}
 
 		// =========
@@ -325,7 +333,7 @@
 		<TotalSupply<T>>::insert(collection.id, total_supply);
 		for (user, amount) in balances {
 			<Balance<T>>::insert((collection.id, &user), amount);
-
+			<PalletStructure<T>>::nest_if_sent_to_token_unchecked(&user, collection.id, TokenId::default());
 			<PalletEvm<T>>::deposit_log(
 				ERC20Events::Transfer {
 					from: H160::default(),
modifiedpallets/nonfungible/src/common.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/common.rs
+++ b/pallets/nonfungible/src/common.rs
@@ -353,6 +353,22 @@
 		<Pallet<T>>::check_nesting(self, sender, from, under, budget)
 	}
 
+	fn nest(
+		&self,
+		under: TokenId,
+		to_nest: (CollectionId, TokenId)
+	) {
+		<Pallet<T>>::nest((self.id, under), to_nest);
+	}
+
+	fn unnest(
+		&self,
+		under: TokenId,
+		to_unnest: (CollectionId, TokenId)
+	) {
+		<Pallet<T>>::unnest((self.id, under), to_unnest);
+	}
+
 	fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId> {
 		<Owned<T>>::iter_prefix((self.id, account))
 			.map(|(id, _)| id)
modifiedpallets/nonfungible/src/lib.rsdiffbeforeafterboth
before · pallets/nonfungible/src/lib.rs
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#![cfg_attr(not(feature = "std"), no_std)]1819use erc::ERC721Events;20use evm_coder::ToLog;21use frame_support::{BoundedVec, ensure, fail, transactional, storage::with_transaction};22use up_data_structs::{23	AccessMode, CollectionId, CustomDataLimit, TokenId, CreateCollectionData, CreateNftExData,24	mapping::TokenAddressMapping, NestingRule, budget::Budget, Property, PropertyPermission,25	PropertyKey, PropertyKeyPermission, Properties, PropertyScope, TrySetProperty,26};27use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};28use pallet_common::{29	Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,30	dispatch::CollectionDispatch, eth::collection_id_to_address,31};32use pallet_structure::Pallet as PalletStructure;33use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};34use sp_core::H160;35use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};36use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap, collections::btree_set::BTreeSet};37use core::ops::Deref;38use codec::{Encode, Decode, MaxEncodedLen};39use scale_info::TypeInfo;4041pub use pallet::*;42#[cfg(feature = "runtime-benchmarks")]43pub mod benchmarking;44pub mod common;45pub mod erc;46pub mod weights;4748pub type CreateItemData<T> = CreateNftExData<<T as pallet_evm::account::Config>::CrossAccountId>;49pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;5051#[struct_versioning::versioned(version = 2, upper)]52#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]53pub struct ItemData<CrossAccountId> {54	#[version(..2)]55	pub const_data: BoundedVec<u8, CustomDataLimit>,5657	#[version(..2)]58	pub variable_data: BoundedVec<u8, CustomDataLimit>,5960	pub owner: CrossAccountId,61}6263#[frame_support::pallet]64pub mod pallet {65	use super::*;66	use frame_support::{67		Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, traits::StorageVersion,68	};69	use frame_system::pallet_prelude::*;70	use up_data_structs::{CollectionId, TokenId};71	use super::weights::WeightInfo;7273	#[pallet::error]74	pub enum Error<T> {75		/// Not Nonfungible item data used to mint in Nonfungible collection.76		NotNonfungibleDataUsedToMintFungibleCollectionToken,77		/// Used amount > 1 with NFT78		NonfungibleItemsHaveNoAmount,79	}8081	#[pallet::config]82	pub trait Config:83		frame_system::Config + pallet_common::Config + pallet_structure::Config + pallet_evm::Config84	{85		type WeightInfo: WeightInfo;86	}8788	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);8990	#[pallet::pallet]91	#[pallet::storage_version(STORAGE_VERSION)]92	#[pallet::generate_store(pub(super) trait Store)]93	pub struct Pallet<T>(_);9495	#[pallet::storage]96	pub type TokensMinted<T: Config> =97		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;98	#[pallet::storage]99	pub type TokensBurnt<T: Config> =100		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;101102	#[pallet::storage]103	pub type TokenData<T: Config> = StorageNMap<104		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),105		Value = ItemData<T::CrossAccountId>,106		QueryKind = OptionQuery,107	>;108109	#[pallet::storage]110	#[pallet::getter(fn token_properties)]111	pub type TokenProperties<T: Config> = StorageNMap<112		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),113		Value = Properties,114		QueryKind = ValueQuery,115		OnEmpty = up_data_structs::TokenProperties,116	>;117118	/// Used to enumerate tokens owned by account119	#[pallet::storage]120	pub type Owned<T: Config> = StorageNMap<121		Key = (122			Key<Twox64Concat, CollectionId>,123			Key<Blake2_128Concat, T::CrossAccountId>,124			Key<Twox64Concat, TokenId>,125		),126		Value = bool,127		QueryKind = ValueQuery,128	>;129130	#[pallet::storage]131	pub type AccountBalance<T: Config> = StorageNMap<132		Key = (133			Key<Twox64Concat, CollectionId>,134			Key<Blake2_128Concat, T::CrossAccountId>,135		),136		Value = u32,137		QueryKind = ValueQuery,138	>;139140	#[pallet::storage]141	pub type Allowance<T: Config> = StorageNMap<142		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),143		Value = T::CrossAccountId,144		QueryKind = OptionQuery,145	>;146147	#[pallet::hooks]148	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {149		fn on_runtime_upgrade() -> Weight {150			if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {151				let mut had_consts = BTreeSet::new();152				<TokenData<T>>::translate::<ItemDataVersion1<T::CrossAccountId>, _>(|(collection, token), v| {153					let mut props = vec![];154					if !v.const_data.is_empty() {155						props.push(Property {156							key: b"_old_constData".to_vec().try_into().unwrap(),157							value: v.const_data.clone().into_inner().try_into().expect("const too long"),158						});159						had_consts.insert(collection);160					}161					if !v.variable_data.is_empty() {162						props.push(Property {163							key: b"_old_variableData".to_vec().try_into().unwrap(),164							value: v.variable_data.clone().into_inner().try_into().expect("variable too long"),165						})166					}167					if !props.is_empty() {168						Self::set_scoped_token_properties(169							collection,170							token,171							PropertyScope::None,172							props.into_iter(),173						).expect("existing token data exceeds property storage");174					}175					Some(<ItemDataVersion2<T::CrossAccountId>>::from(v))176				});177				for collection in had_consts {178					<PalletCommon<T>>::set_property_permission_unchecked(179						collection,180						PropertyKeyPermission {181							key: b"_old_constData".to_vec().try_into().unwrap(),182							permission: PropertyPermission {183								mutable: false,184								collection_admin: true,185								token_owner: false,186							},187						}188					).expect("failed to configure permission");189				}190			}191192			0193		}194	}195}196197pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);198impl<T: Config> NonfungibleHandle<T> {199	pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {200		Self(inner)201	}202	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {203		self.0204	}205	pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {206		&mut self.0207	}208}209impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {210	fn recorder(&self) -> &SubstrateRecorder<T> {211		self.0.recorder()212	}213	fn into_recorder(self) -> SubstrateRecorder<T> {214		self.0.into_recorder()215	}216}217impl<T: Config> Deref for NonfungibleHandle<T> {218	type Target = pallet_common::CollectionHandle<T>;219220	fn deref(&self) -> &Self::Target {221		&self.0222	}223}224225impl<T: Config> Pallet<T> {226	pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {227		<TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)228	}229	pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {230		<TokenData<T>>::contains_key((collection.id, token))231	}232233	pub fn set_scoped_token_property(234		collection_id: CollectionId,235		token_id: TokenId,236		scope: PropertyScope,237		property: Property,238	) -> DispatchResult {239		TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {240			properties.try_scoped_set(scope, property.key, property.value)241		})242		.map_err(<CommonError<T>>::from)?;243244		Ok(())245	}246247	pub fn set_scoped_token_properties(248		collection_id: CollectionId,249		token_id: TokenId,250		scope: PropertyScope,251		properties: impl Iterator<Item=Property>,252	) -> DispatchResult {253		TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {254			stored_properties.try_scoped_set_from_iter(scope, properties)255		})256		.map_err(<CommonError<T>>::from)?;257258		Ok(())259	}260261	pub fn current_token_id(collection_id: CollectionId) -> TokenId {262		TokenId(<TokensMinted<T>>::get(collection_id))263	}264}265266// unchecked calls skips any permission checks267impl<T: Config> Pallet<T> {268	pub fn init_collection(269		owner: T::AccountId,270		data: CreateCollectionData<T::AccountId>,271	) -> Result<CollectionId, DispatchError> {272		<PalletCommon<T>>::init_collection(owner, data)273	}274	pub fn destroy_collection(275		collection: NonfungibleHandle<T>,276		sender: &T::CrossAccountId,277	) -> DispatchResult {278		let id = collection.id;279280		// =========281282		PalletCommon::destroy_collection(collection.0, sender)?;283284		<TokenData<T>>::remove_prefix((id,), None);285		<Owned<T>>::remove_prefix((id,), None);286		<TokensMinted<T>>::remove(id);287		<TokensBurnt<T>>::remove(id);288		<Allowance<T>>::remove_prefix((id,), None);289		<AccountBalance<T>>::remove_prefix((id,), None);290		Ok(())291	}292293	pub fn burn(294		collection: &NonfungibleHandle<T>,295		sender: &T::CrossAccountId,296		token: TokenId,297	) -> DispatchResult {298		let token_data =299			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;300		ensure!(301			&token_data.owner == sender302				|| (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(sender)),303			<CommonError<T>>::NoPermission304		);305306		if collection.permissions.access() == AccessMode::AllowList {307			collection.check_allowlist(sender)?;308		}309310		let burnt = <TokensBurnt<T>>::get(collection.id)311			.checked_add(1)312			.ok_or(ArithmeticError::Overflow)?;313314		let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))315			.checked_sub(1)316			.ok_or(ArithmeticError::Overflow)?;317318		if balance == 0 {319			<AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));320		} else {321			<AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);322		}323		// =========324325		<Owned<T>>::remove((collection.id, &token_data.owner, token));326		<TokensBurnt<T>>::insert(collection.id, burnt);327		<TokenData<T>>::remove((collection.id, token));328		<TokenProperties<T>>::remove((collection.id, token));329		let old_spender = <Allowance<T>>::take((collection.id, token));330331		if let Some(old_spender) = old_spender {332			<PalletCommon<T>>::deposit_event(CommonEvent::Approved(333				collection.id,334				token,335				sender.clone(),336				old_spender,337				0,338			));339		}340341		<PalletEvm<T>>::deposit_log(342			ERC721Events::Transfer {343				from: *token_data.owner.as_eth(),344				to: H160::default(),345				token_id: token.into(),346			}347			.to_log(collection_id_to_address(collection.id)),348		);349		<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(350			collection.id,351			token,352			token_data.owner,353			1,354		));355		Ok(())356	}357358	pub fn set_token_property(359		collection: &NonfungibleHandle<T>,360		sender: &T::CrossAccountId,361		token_id: TokenId,362		property: Property,363	) -> DispatchResult {364		Self::check_token_change_permission(collection, sender, token_id, &property.key)?;365366		<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {367			let property = property.clone();368			properties.try_set(property.key, property.value)369		})370		.map_err(<CommonError<T>>::from)?;371372		<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(373			collection.id,374			token_id,375			property.key,376		));377378		Ok(())379	}380381	#[transactional]382	pub fn set_token_properties(383		collection: &NonfungibleHandle<T>,384		sender: &T::CrossAccountId,385		token_id: TokenId,386		properties: Vec<Property>,387	) -> DispatchResult {388		for property in properties {389			Self::set_token_property(collection, sender, token_id, property)?;390		}391392		Ok(())393	}394395	pub fn delete_token_property(396		collection: &NonfungibleHandle<T>,397		sender: &T::CrossAccountId,398		token_id: TokenId,399		property_key: PropertyKey,400	) -> DispatchResult {401		Self::check_token_change_permission(collection, sender, token_id, &property_key)?;402403		<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {404			properties.remove(&property_key)405		})406		.map_err(<CommonError<T>>::from)?;407408		<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(409			collection.id,410			token_id,411			property_key,412		));413414		Ok(())415	}416417	fn check_token_change_permission(418		collection: &NonfungibleHandle<T>,419		sender: &T::CrossAccountId,420		token_id: TokenId,421		property_key: &PropertyKey,422	) -> DispatchResult {423		let permission = <PalletCommon<T>>::property_permissions(collection.id)424			.get(property_key)425			.cloned()426			.unwrap_or_else(PropertyPermission::none);427428		let token_data = <TokenData<T>>::get((collection.id, token_id))429			.ok_or(<CommonError<T>>::TokenNotFound)?;430431		let check_token_owner = || -> DispatchResult {432			ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);433			Ok(())434		};435436		let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))437			.get(property_key)438			.is_some();439440		match permission {441			PropertyPermission { mutable: false, .. } if is_property_exists => {442				Err(<CommonError<T>>::NoPermission.into())443			}444445			PropertyPermission {446				collection_admin,447				token_owner,448				..449			} => {450				let mut check_result = Err(<CommonError<T>>::NoPermission.into());451452				if collection_admin {453					check_result = collection.check_is_owner_or_admin(sender);454				}455456				if token_owner {457					check_result.or_else(|_| check_token_owner())458				} else {459					check_result460				}461			}462		}463	}464465	#[transactional]466	pub fn delete_token_properties(467		collection: &NonfungibleHandle<T>,468		sender: &T::CrossAccountId,469		token_id: TokenId,470		property_keys: Vec<PropertyKey>,471	) -> DispatchResult {472		for key in property_keys {473			Self::delete_token_property(collection, sender, token_id, key)?;474		}475476		Ok(())477	}478479	pub fn set_collection_properties(480		collection: &NonfungibleHandle<T>,481		sender: &T::CrossAccountId,482		properties: Vec<Property>,483	) -> DispatchResult {484		<PalletCommon<T>>::set_collection_properties(collection, sender, properties)485	}486487	pub fn delete_collection_properties(488		collection: &CollectionHandle<T>,489		sender: &T::CrossAccountId,490		property_keys: Vec<PropertyKey>,491	) -> DispatchResult {492		<PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)493	}494495	pub fn set_property_permissions(496		collection: &CollectionHandle<T>,497		sender: &T::CrossAccountId,498		property_permissions: Vec<PropertyKeyPermission>,499	) -> DispatchResult {500		<PalletCommon<T>>::set_property_permissions(collection, sender, property_permissions)501	}502503	pub fn set_property_permission(504		collection: &CollectionHandle<T>,505		sender: &T::CrossAccountId,506		permission: PropertyKeyPermission,507	) -> DispatchResult {508		<PalletCommon<T>>::set_property_permission(collection, sender, permission)509	}510511	pub fn transfer(512		collection: &NonfungibleHandle<T>,513		from: &T::CrossAccountId,514		to: &T::CrossAccountId,515		token: TokenId,516		nesting_budget: &dyn Budget,517	) -> DispatchResult {518		ensure!(519			collection.limits.transfers_enabled(),520			<CommonError<T>>::TransferNotAllowed521		);522523		let token_data =524			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;525		// TODO: require sender to be token, owner, require admins to go through transfer_from526		ensure!(527			&token_data.owner == from528				|| (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(from)),529			<CommonError<T>>::NoPermission530		);531532		if collection.permissions.access() == AccessMode::AllowList {533			collection.check_allowlist(from)?;534			collection.check_allowlist(to)?;535		}536		<PalletCommon<T>>::ensure_correct_receiver(to)?;537538		let balance_from = <AccountBalance<T>>::get((collection.id, from))539			.checked_sub(1)540			.ok_or(<CommonError<T>>::TokenValueTooLow)?;541		let balance_to = if from != to {542			let balance_to = <AccountBalance<T>>::get((collection.id, to))543				.checked_add(1)544				.ok_or(ArithmeticError::Overflow)?;545546			ensure!(547				balance_to < collection.limits.account_token_ownership_limit(),548				<CommonError<T>>::AccountTokenLimitExceeded,549			);550551			Some(balance_to)552		} else {553			None554		};555556		if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {557			let handle = <CollectionHandle<T>>::try_get(target.0)?;558			let dispatch = T::CollectionDispatch::dispatch(handle);559			let dispatch = dispatch.as_dyn();560561			dispatch.check_nesting(562				from.clone(),563				(collection.id, token),564				target.1,565				nesting_budget,566			)?;567		}568569		// =========570571		<TokenData<T>>::insert(572			(collection.id, token),573			ItemData {574				owner: to.clone(),575				..token_data576			},577		);578579		if let Some(balance_to) = balance_to {580			// from != to581			if balance_from == 0 {582				<AccountBalance<T>>::remove((collection.id, from));583			} else {584				<AccountBalance<T>>::insert((collection.id, from), balance_from);585			}586			<AccountBalance<T>>::insert((collection.id, to), balance_to);587			<Owned<T>>::remove((collection.id, from, token));588			<Owned<T>>::insert((collection.id, to, token), true);589		}590		Self::set_allowance_unchecked(collection, from, token, None, true);591592		<PalletEvm<T>>::deposit_log(593			ERC721Events::Transfer {594				from: *from.as_eth(),595				to: *to.as_eth(),596				token_id: token.into(),597			}598			.to_log(collection_id_to_address(collection.id)),599		);600		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(601			collection.id,602			token,603			from.clone(),604			to.clone(),605			1,606		));607		Ok(())608	}609610	pub fn create_multiple_items(611		collection: &NonfungibleHandle<T>,612		sender: &T::CrossAccountId,613		data: Vec<CreateItemData<T>>,614		nesting_budget: &dyn Budget,615	) -> DispatchResult {616		if !collection.is_owner_or_admin(sender) {617			ensure!(618				collection.permissions.mint_mode(),619				<CommonError<T>>::PublicMintingNotAllowed620			);621			collection.check_allowlist(sender)?;622623			for item in data.iter() {624				collection.check_allowlist(&item.owner)?;625			}626		}627628		for data in data.iter() {629			<PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;630		}631632		let first_token = <TokensMinted<T>>::get(collection.id);633		let tokens_minted = first_token634			.checked_add(data.len() as u32)635			.ok_or(ArithmeticError::Overflow)?;636		ensure!(637			tokens_minted <= collection.limits.token_limit(),638			<CommonError<T>>::CollectionTokenLimitExceeded639		);640641		let mut balances = BTreeMap::new();642		for data in &data {643			let balance = balances644				.entry(&data.owner)645				.or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));646			*balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;647648			ensure!(649				*balance <= collection.limits.account_token_ownership_limit(),650				<CommonError<T>>::AccountTokenLimitExceeded,651			);652		}653654		for (i, data) in data.iter().enumerate() {655			let token = TokenId(first_token + i as u32 + 1);656			if let Some(target) = T::CrossTokenAddressMapping::address_to_token(&data.owner) {657				let handle = <CollectionHandle<T>>::try_get(target.0)?;658				let dispatch = T::CollectionDispatch::dispatch(handle);659				let dispatch = dispatch.as_dyn();660				dispatch.check_nesting(661					sender.clone(),662					(collection.id, token),663					target.1,664					nesting_budget,665				)?;666			}667		}668669		// =========670671		with_transaction(|| {672			for (i, data) in data.iter().enumerate() {673				let token = first_token + i as u32 + 1;674675				<TokenData<T>>::insert(676					(collection.id, token),677					ItemData {678						// const_data: data.const_data.clone(),679						owner: data.owner.clone(),680					},681				);682683				if let Err(e) = Self::set_token_properties(684					collection,685					sender,686					TokenId(token),687					data.properties.clone().into_inner(),688				) {689					return TransactionOutcome::Rollback(Err(e));690				}691			}692			TransactionOutcome::Commit(Ok(()))693		})?;694695		<TokensMinted<T>>::insert(collection.id, tokens_minted);696		for (account, balance) in balances {697			<AccountBalance<T>>::insert((collection.id, account), balance);698		}699		for (i, data) in data.into_iter().enumerate() {700			let token = first_token + i as u32 + 1;701			<Owned<T>>::insert((collection.id, &data.owner, token), true);702703			<PalletEvm<T>>::deposit_log(704				ERC721Events::Transfer {705					from: H160::default(),706					to: *data.owner.as_eth(),707					token_id: token.into(),708				}709				.to_log(collection_id_to_address(collection.id)),710			);711			<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(712				collection.id,713				TokenId(token),714				data.owner.clone(),715				1,716			));717		}718		Ok(())719	}720721	pub fn set_allowance_unchecked(722		collection: &NonfungibleHandle<T>,723		sender: &T::CrossAccountId,724		token: TokenId,725		spender: Option<&T::CrossAccountId>,726		assume_implicit_eth: bool,727	) {728		if let Some(spender) = spender {729			let old_spender = <Allowance<T>>::get((collection.id, token));730			<Allowance<T>>::insert((collection.id, token), spender);731			// In ERC721 there is only one possible approved user of token, so we set732			// approved user to spender733			<PalletEvm<T>>::deposit_log(734				ERC721Events::Approval {735					owner: *sender.as_eth(),736					approved: *spender.as_eth(),737					token_id: token.into(),738				}739				.to_log(collection_id_to_address(collection.id)),740			);741			// In Unique chain, any token can have any amount of approved users, so we need to742			// set allowance of old owner to 0, and allowance of new owner to 1743			if old_spender.as_ref() != Some(spender) {744				if let Some(old_owner) = old_spender {745					<PalletCommon<T>>::deposit_event(CommonEvent::Approved(746						collection.id,747						token,748						sender.clone(),749						old_owner,750						0,751					));752				}753				<PalletCommon<T>>::deposit_event(CommonEvent::Approved(754					collection.id,755					token,756					sender.clone(),757					spender.clone(),758					1,759				));760			}761		} else {762			let old_spender = <Allowance<T>>::take((collection.id, token));763			if !assume_implicit_eth {764				// In ERC721 there is only one possible approved user of token, so we set765				// approved user to zero address766				<PalletEvm<T>>::deposit_log(767					ERC721Events::Approval {768						owner: *sender.as_eth(),769						approved: H160::default(),770						token_id: token.into(),771					}772					.to_log(collection_id_to_address(collection.id)),773				);774			}775			// In Unique chain, any token can have any amount of approved users, so we need to776			// set allowance of old owner to 0777			if let Some(old_spender) = old_spender {778				<PalletCommon<T>>::deposit_event(CommonEvent::Approved(779					collection.id,780					token,781					sender.clone(),782					old_spender,783					0,784				));785			}786		}787	}788789	pub fn set_allowance(790		collection: &NonfungibleHandle<T>,791		sender: &T::CrossAccountId,792		token: TokenId,793		spender: Option<&T::CrossAccountId>,794	) -> DispatchResult {795		if collection.permissions.access() == AccessMode::AllowList {796			collection.check_allowlist(sender)?;797			if let Some(spender) = spender {798				collection.check_allowlist(spender)?;799			}800		}801802		if let Some(spender) = spender {803			<PalletCommon<T>>::ensure_correct_receiver(spender)?;804		}805		let token_data =806			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;807		if &token_data.owner != sender {808			ensure!(809				collection.ignores_owned_amount(sender),810				<CommonError<T>>::CantApproveMoreThanOwned811			);812		}813814		// =========815816		Self::set_allowance_unchecked(collection, sender, token, spender, false);817		Ok(())818	}819820	fn check_allowed(821		collection: &NonfungibleHandle<T>,822		spender: &T::CrossAccountId,823		from: &T::CrossAccountId,824		token: TokenId,825		nesting_budget: &dyn Budget,826	) -> DispatchResult {827		if spender.conv_eq(from) {828			return Ok(());829		}830		if collection.permissions.access() == AccessMode::AllowList {831			// `from`, `to` checked in [`transfer`]832			collection.check_allowlist(spender)?;833		}834		if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {835			// TODO: should collection owner be allowed to perform this transfer?836			ensure!(837				<PalletStructure<T>>::check_indirectly_owned(838					spender.clone(),839					source.0,840					source.1,841					None,842					nesting_budget843				)?,844				<CommonError<T>>::ApprovedValueTooLow,845			);846			return Ok(());847		}848		if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {849			return Ok(());850		}851		ensure!(852			collection.ignores_allowance(spender),853			<CommonError<T>>::ApprovedValueTooLow854		);855		Ok(())856	}857858	pub fn transfer_from(859		collection: &NonfungibleHandle<T>,860		spender: &T::CrossAccountId,861		from: &T::CrossAccountId,862		to: &T::CrossAccountId,863		token: TokenId,864		nesting_budget: &dyn Budget,865	) -> DispatchResult {866		Self::check_allowed(collection, spender, from, token, nesting_budget)?;867868		// =========869870		// Allowance is reset in [`transfer`]871		Self::transfer(collection, from, to, token, nesting_budget)872	}873874	pub fn burn_from(875		collection: &NonfungibleHandle<T>,876		spender: &T::CrossAccountId,877		from: &T::CrossAccountId,878		token: TokenId,879		nesting_budget: &dyn Budget,880	) -> DispatchResult {881		Self::check_allowed(collection, spender, from, token, nesting_budget)?;882883		// =========884885		Self::burn(collection, from, token)886	}887888	pub fn check_nesting(889		handle: &NonfungibleHandle<T>,890		sender: T::CrossAccountId,891		from: (CollectionId, TokenId),892		under: TokenId,893		nesting_budget: &dyn Budget,894	) -> DispatchResult {895		fn ensure_sender_allowed<T: Config>(896			collection: CollectionId,897			token: TokenId,898			for_nest: (CollectionId, TokenId),899			sender: T::CrossAccountId,900			budget: &dyn Budget,901		) -> DispatchResult {902			ensure!(903				<PalletStructure<T>>::check_indirectly_owned(904					sender,905					collection,906					token,907					Some(for_nest),908					budget909				)?,910				<CommonError<T>>::OnlyOwnerAllowedToNest,911			);912			Ok(())913		}914		match handle.permissions.nesting() {915			NestingRule::Disabled => fail!(<CommonError<T>>::NestingIsDisabled),916			NestingRule::Owner => {917				ensure_sender_allowed::<T>(handle.id, under, from, sender, nesting_budget)?918			}919			NestingRule::OwnerRestricted(whitelist) => {920				ensure!(921					whitelist.contains(&from.0),922					<CommonError<T>>::SourceCollectionIsNotAllowedToNest923				);924				ensure_sender_allowed::<T>(handle.id, under, from, sender, nesting_budget)?925			}926		}927		Ok(())928	}929930	/// Delegated to `create_multiple_items`931	pub fn create_item(932		collection: &NonfungibleHandle<T>,933		sender: &T::CrossAccountId,934		data: CreateItemData<T>,935		nesting_budget: &dyn Budget,936	) -> DispatchResult {937		Self::create_multiple_items(collection, sender, vec![data], nesting_budget)938	}939}
after · pallets/nonfungible/src/lib.rs
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#![cfg_attr(not(feature = "std"), no_std)]1819use erc::ERC721Events;20use evm_coder::ToLog;21use frame_support::{BoundedVec, ensure, fail, transactional, storage::with_transaction};22use up_data_structs::{23	AccessMode, CollectionId, CustomDataLimit, TokenId, CreateCollectionData, CreateNftExData,24	mapping::TokenAddressMapping, NestingRule, budget::Budget, Property, PropertyPermission,25	PropertyKey, PropertyKeyPermission, Properties, PropertyScope, TrySetProperty,26};27use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};28use pallet_common::{29	Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,30	eth::collection_id_to_address,31};32use pallet_structure::Pallet as PalletStructure;33use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};34use sp_core::H160;35use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};36use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap, collections::btree_set::BTreeSet};37use core::ops::Deref;38use codec::{Encode, Decode, MaxEncodedLen};39use scale_info::TypeInfo;4041pub use pallet::*;42#[cfg(feature = "runtime-benchmarks")]43pub mod benchmarking;44pub mod common;45pub mod erc;46pub mod weights;4748pub type CreateItemData<T> = CreateNftExData<<T as pallet_evm::account::Config>::CrossAccountId>;49pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;5051#[struct_versioning::versioned(version = 2, upper)]52#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]53pub struct ItemData<CrossAccountId> {54	#[version(..2)]55	pub const_data: BoundedVec<u8, CustomDataLimit>,5657	#[version(..2)]58	pub variable_data: BoundedVec<u8, CustomDataLimit>,5960	pub owner: CrossAccountId,61}6263#[frame_support::pallet]64pub mod pallet {65	use super::*;66	use frame_support::{67		Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, traits::StorageVersion,68	};69	use frame_system::pallet_prelude::*;70	use up_data_structs::{CollectionId, TokenId};71	use super::weights::WeightInfo;7273	#[pallet::error]74	pub enum Error<T> {75		/// Not Nonfungible item data used to mint in Nonfungible collection.76		NotNonfungibleDataUsedToMintFungibleCollectionToken,77		/// Used amount > 1 with NFT78		NonfungibleItemsHaveNoAmount,79		/// Unable to burn NFT with children80		CantBurnNftWithChildren,81	}8283	#[pallet::config]84	pub trait Config:85		frame_system::Config + pallet_common::Config + pallet_structure::Config + pallet_evm::Config86	{87		type WeightInfo: WeightInfo;88	}8990	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);9192	#[pallet::pallet]93	#[pallet::storage_version(STORAGE_VERSION)]94	#[pallet::generate_store(pub(super) trait Store)]95	pub struct Pallet<T>(_);9697	#[pallet::storage]98	pub type TokensMinted<T: Config> =99		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;100	#[pallet::storage]101	pub type TokensBurnt<T: Config> =102		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;103104	#[pallet::storage]105	pub type TokenData<T: Config> = StorageNMap<106		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),107		Value = ItemData<T::CrossAccountId>,108		QueryKind = OptionQuery,109	>;110111	#[pallet::storage]112	#[pallet::getter(fn token_properties)]113	pub type TokenProperties<T: Config> = StorageNMap<114		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),115		Value = Properties,116		QueryKind = ValueQuery,117		OnEmpty = up_data_structs::TokenProperties,118	>;119120	/// Used to enumerate tokens owned by account121	#[pallet::storage]122	pub type Owned<T: Config> = StorageNMap<123		Key = (124			Key<Twox64Concat, CollectionId>,125			Key<Blake2_128Concat, T::CrossAccountId>,126			Key<Twox64Concat, TokenId>,127		),128		Value = bool,129		QueryKind = ValueQuery,130	>;131132	/// Used to enumerate token's children133	#[pallet::storage]134	#[pallet::getter(fn token_children)]135	pub type TokenChildren<T: Config> = StorageNMap<136		Key = (137			Key<Twox64Concat, CollectionId>,138			Key<Twox64Concat, TokenId>,139			Key<Twox64Concat, (CollectionId, TokenId)>,140		),141		Value = bool,142		QueryKind = ValueQuery,143	>;144145	#[pallet::storage]146	pub type AccountBalance<T: Config> = StorageNMap<147		Key = (148			Key<Twox64Concat, CollectionId>,149			Key<Blake2_128Concat, T::CrossAccountId>,150		),151		Value = u32,152		QueryKind = ValueQuery,153	>;154155	#[pallet::storage]156	pub type Allowance<T: Config> = StorageNMap<157		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),158		Value = T::CrossAccountId,159		QueryKind = OptionQuery,160	>;161162	#[pallet::hooks]163	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {164		fn on_runtime_upgrade() -> Weight {165			if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {166				let mut had_consts = BTreeSet::new();167				<TokenData<T>>::translate::<ItemDataVersion1<T::CrossAccountId>, _>(|(collection, token), v| {168					let mut props = vec![];169					if !v.const_data.is_empty() {170						props.push(Property {171							key: b"_old_constData".to_vec().try_into().unwrap(),172							value: v.const_data.clone().into_inner().try_into().expect("const too long"),173						});174						had_consts.insert(collection);175					}176					if !v.variable_data.is_empty() {177						props.push(Property {178							key: b"_old_variableData".to_vec().try_into().unwrap(),179							value: v.variable_data.clone().into_inner().try_into().expect("variable too long"),180						})181					}182					if !props.is_empty() {183						Self::set_scoped_token_properties(184							collection,185							token,186							PropertyScope::None,187							props.into_iter(),188						).expect("existing token data exceeds property storage");189					}190					Some(<ItemDataVersion2<T::CrossAccountId>>::from(v))191				});192				for collection in had_consts {193					<PalletCommon<T>>::set_property_permission_unchecked(194						collection,195						PropertyKeyPermission {196							key: b"_old_constData".to_vec().try_into().unwrap(),197							permission: PropertyPermission {198								mutable: false,199								collection_admin: true,200								token_owner: false,201							},202						}203					).expect("failed to configure permission");204				}205			}206207			0208		}209	}210}211212pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);213impl<T: Config> NonfungibleHandle<T> {214	pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {215		Self(inner)216	}217	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {218		self.0219	}220	pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {221		&mut self.0222	}223}224impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {225	fn recorder(&self) -> &SubstrateRecorder<T> {226		self.0.recorder()227	}228	fn into_recorder(self) -> SubstrateRecorder<T> {229		self.0.into_recorder()230	}231}232impl<T: Config> Deref for NonfungibleHandle<T> {233	type Target = pallet_common::CollectionHandle<T>;234235	fn deref(&self) -> &Self::Target {236		&self.0237	}238}239240impl<T: Config> Pallet<T> {241	pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {242		<TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)243	}244	pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {245		<TokenData<T>>::contains_key((collection.id, token))246	}247248	pub fn set_scoped_token_property(249		collection_id: CollectionId,250		token_id: TokenId,251		scope: PropertyScope,252		property: Property,253	) -> DispatchResult {254		TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {255			properties.try_scoped_set(scope, property.key, property.value)256		})257		.map_err(<CommonError<T>>::from)?;258259		Ok(())260	}261262	pub fn set_scoped_token_properties(263		collection_id: CollectionId,264		token_id: TokenId,265		scope: PropertyScope,266		properties: impl Iterator<Item=Property>,267	) -> DispatchResult {268		TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {269			stored_properties.try_scoped_set_from_iter(scope, properties)270		})271		.map_err(<CommonError<T>>::from)?;272273		Ok(())274	}275276	pub fn current_token_id(collection_id: CollectionId) -> TokenId {277		TokenId(<TokensMinted<T>>::get(collection_id))278	}279}280281// unchecked calls skips any permission checks282impl<T: Config> Pallet<T> {283	pub fn init_collection(284		owner: T::AccountId,285		data: CreateCollectionData<T::AccountId>,286	) -> Result<CollectionId, DispatchError> {287		<PalletCommon<T>>::init_collection(owner, data)288	}289	pub fn destroy_collection(290		collection: NonfungibleHandle<T>,291		sender: &T::CrossAccountId,292	) -> DispatchResult {293		let id = collection.id;294295		if Self::collection_has_tokens(id) {296			return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());297		}298299		// =========300301		PalletCommon::destroy_collection(collection.0, sender)?;302303		<TokenData<T>>::remove_prefix((id,), None);304		<TokenChildren<T>>::remove_prefix((id,), None);305		<Owned<T>>::remove_prefix((id,), None);306		<TokensMinted<T>>::remove(id);307		<TokensBurnt<T>>::remove(id);308		<Allowance<T>>::remove_prefix((id,), None);309		<AccountBalance<T>>::remove_prefix((id,), None);310		Ok(())311	}312313	pub fn burn(314		collection: &NonfungibleHandle<T>,315		sender: &T::CrossAccountId,316		token: TokenId,317	) -> DispatchResult {318		let token_data =319			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;320		ensure!(321			&token_data.owner == sender322				|| (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(sender)),323			<CommonError<T>>::NoPermission324		);325326		if collection.permissions.access() == AccessMode::AllowList {327			collection.check_allowlist(sender)?;328		}329330		if Self::token_has_children(collection.id, token) {331			return Err(<Error<T>>::CantBurnNftWithChildren.into());332		}333334		let burnt = <TokensBurnt<T>>::get(collection.id)335			.checked_add(1)336			.ok_or(ArithmeticError::Overflow)?;337338		let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))339			.checked_sub(1)340			.ok_or(ArithmeticError::Overflow)?;341342		// =========343344		if balance == 0 {345			<AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));346		} else {347			<AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);348		}349350		<PalletStructure<T>>::unnest_if_nested(351			&token_data.owner,352			collection.id,353			token354		);355356		<Owned<T>>::remove((collection.id, &token_data.owner, token));357		<TokensBurnt<T>>::insert(collection.id, burnt);358		<TokenData<T>>::remove((collection.id, token));359		<TokenProperties<T>>::remove((collection.id, token));360		let old_spender = <Allowance<T>>::take((collection.id, token));361362		if let Some(old_spender) = old_spender {363			<PalletCommon<T>>::deposit_event(CommonEvent::Approved(364				collection.id,365				token,366				sender.clone(),367				old_spender,368				0,369			));370		}371372		<PalletEvm<T>>::deposit_log(373			ERC721Events::Transfer {374				from: *token_data.owner.as_eth(),375				to: H160::default(),376				token_id: token.into(),377			}378			.to_log(collection_id_to_address(collection.id)),379		);380		<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(381			collection.id,382			token,383			token_data.owner,384			1,385		));386		Ok(())387	}388389	pub fn set_token_property(390		collection: &NonfungibleHandle<T>,391		sender: &T::CrossAccountId,392		token_id: TokenId,393		property: Property,394	) -> DispatchResult {395		Self::check_token_change_permission(collection, sender, token_id, &property.key)?;396397		<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {398			let property = property.clone();399			properties.try_set(property.key, property.value)400		})401		.map_err(<CommonError<T>>::from)?;402403		<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(404			collection.id,405			token_id,406			property.key,407		));408409		Ok(())410	}411412	#[transactional]413	pub fn set_token_properties(414		collection: &NonfungibleHandle<T>,415		sender: &T::CrossAccountId,416		token_id: TokenId,417		properties: Vec<Property>,418	) -> DispatchResult {419		for property in properties {420			Self::set_token_property(collection, sender, token_id, property)?;421		}422423		Ok(())424	}425426	pub fn delete_token_property(427		collection: &NonfungibleHandle<T>,428		sender: &T::CrossAccountId,429		token_id: TokenId,430		property_key: PropertyKey,431	) -> DispatchResult {432		Self::check_token_change_permission(collection, sender, token_id, &property_key)?;433434		<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {435			properties.remove(&property_key)436		})437		.map_err(<CommonError<T>>::from)?;438439		<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(440			collection.id,441			token_id,442			property_key,443		));444445		Ok(())446	}447448	fn check_token_change_permission(449		collection: &NonfungibleHandle<T>,450		sender: &T::CrossAccountId,451		token_id: TokenId,452		property_key: &PropertyKey,453	) -> DispatchResult {454		let permission = <PalletCommon<T>>::property_permissions(collection.id)455			.get(property_key)456			.cloned()457			.unwrap_or_else(PropertyPermission::none);458459		let token_data = <TokenData<T>>::get((collection.id, token_id))460			.ok_or(<CommonError<T>>::TokenNotFound)?;461462		let check_token_owner = || -> DispatchResult {463			ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);464			Ok(())465		};466467		let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))468			.get(property_key)469			.is_some();470471		match permission {472			PropertyPermission { mutable: false, .. } if is_property_exists => {473				Err(<CommonError<T>>::NoPermission.into())474			}475476			PropertyPermission {477				collection_admin,478				token_owner,479				..480			} => {481				let mut check_result = Err(<CommonError<T>>::NoPermission.into());482483				if collection_admin {484					check_result = collection.check_is_owner_or_admin(sender);485				}486487				if token_owner {488					check_result.or_else(|_| check_token_owner())489				} else {490					check_result491				}492			}493		}494	}495496	#[transactional]497	pub fn delete_token_properties(498		collection: &NonfungibleHandle<T>,499		sender: &T::CrossAccountId,500		token_id: TokenId,501		property_keys: Vec<PropertyKey>,502	) -> DispatchResult {503		for key in property_keys {504			Self::delete_token_property(collection, sender, token_id, key)?;505		}506507		Ok(())508	}509510	pub fn set_collection_properties(511		collection: &NonfungibleHandle<T>,512		sender: &T::CrossAccountId,513		properties: Vec<Property>,514	) -> DispatchResult {515		<PalletCommon<T>>::set_collection_properties(collection, sender, properties)516	}517518	pub fn delete_collection_properties(519		collection: &CollectionHandle<T>,520		sender: &T::CrossAccountId,521		property_keys: Vec<PropertyKey>,522	) -> DispatchResult {523		<PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)524	}525526	pub fn set_property_permissions(527		collection: &CollectionHandle<T>,528		sender: &T::CrossAccountId,529		property_permissions: Vec<PropertyKeyPermission>,530	) -> DispatchResult {531		<PalletCommon<T>>::set_property_permissions(collection, sender, property_permissions)532	}533534	pub fn set_property_permission(535		collection: &CollectionHandle<T>,536		sender: &T::CrossAccountId,537		permission: PropertyKeyPermission,538	) -> DispatchResult {539		<PalletCommon<T>>::set_property_permission(collection, sender, permission)540	}541542	pub fn transfer(543		collection: &NonfungibleHandle<T>,544		from: &T::CrossAccountId,545		to: &T::CrossAccountId,546		token: TokenId,547		nesting_budget: &dyn Budget,548	) -> DispatchResult {549		ensure!(550			collection.limits.transfers_enabled(),551			<CommonError<T>>::TransferNotAllowed552		);553554		let token_data =555			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;556		// TODO: require sender to be token, owner, require admins to go through transfer_from557		ensure!(558			&token_data.owner == from559				|| (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(from)),560			<CommonError<T>>::NoPermission561		);562563		if collection.permissions.access() == AccessMode::AllowList {564			collection.check_allowlist(from)?;565			collection.check_allowlist(to)?;566		}567		<PalletCommon<T>>::ensure_correct_receiver(to)?;568569		let balance_from = <AccountBalance<T>>::get((collection.id, from))570			.checked_sub(1)571			.ok_or(<CommonError<T>>::TokenValueTooLow)?;572		let balance_to = if from != to {573			let balance_to = <AccountBalance<T>>::get((collection.id, to))574				.checked_add(1)575				.ok_or(ArithmeticError::Overflow)?;576577			ensure!(578				balance_to < collection.limits.account_token_ownership_limit(),579				<CommonError<T>>::AccountTokenLimitExceeded,580			);581582			Some(balance_to)583		} else {584			None585		};586587		<PalletStructure<T>>::nest_if_sent_to_token(588			from.clone(),589			to,590			collection.id,591			token,592			nesting_budget593		)?;594595		// =========596597		<PalletStructure<T>>::unnest_if_nested(598			from,599			collection.id,600			token601		);602603		<TokenData<T>>::insert(604			(collection.id, token),605			ItemData {606				owner: to.clone(),607				..token_data608			},609		);610611		if let Some(balance_to) = balance_to {612			// from != to613			if balance_from == 0 {614				<AccountBalance<T>>::remove((collection.id, from));615			} else {616				<AccountBalance<T>>::insert((collection.id, from), balance_from);617			}618			<AccountBalance<T>>::insert((collection.id, to), balance_to);619			<Owned<T>>::remove((collection.id, from, token));620			<Owned<T>>::insert((collection.id, to, token), true);621		}622		Self::set_allowance_unchecked(collection, from, token, None, true);623624		<PalletEvm<T>>::deposit_log(625			ERC721Events::Transfer {626				from: *from.as_eth(),627				to: *to.as_eth(),628				token_id: token.into(),629			}630			.to_log(collection_id_to_address(collection.id)),631		);632		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(633			collection.id,634			token,635			from.clone(),636			to.clone(),637			1,638		));639		Ok(())640	}641642	pub fn create_multiple_items(643		collection: &NonfungibleHandle<T>,644		sender: &T::CrossAccountId,645		data: Vec<CreateItemData<T>>,646		nesting_budget: &dyn Budget,647	) -> DispatchResult {648		if !collection.is_owner_or_admin(sender) {649			ensure!(650				collection.permissions.mint_mode(),651				<CommonError<T>>::PublicMintingNotAllowed652			);653			collection.check_allowlist(sender)?;654655			for item in data.iter() {656				collection.check_allowlist(&item.owner)?;657			}658		}659660		for data in data.iter() {661			<PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;662		}663664		let first_token = <TokensMinted<T>>::get(collection.id);665		let tokens_minted = first_token666			.checked_add(data.len() as u32)667			.ok_or(ArithmeticError::Overflow)?;668		ensure!(669			tokens_minted <= collection.limits.token_limit(),670			<CommonError<T>>::CollectionTokenLimitExceeded671		);672673		let mut balances = BTreeMap::new();674		for data in &data {675			let balance = balances676				.entry(&data.owner)677				.or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));678			*balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;679680			ensure!(681				*balance <= collection.limits.account_token_ownership_limit(),682				<CommonError<T>>::AccountTokenLimitExceeded,683			);684		}685686		for (i, data) in data.iter().enumerate() {687			let token = TokenId(first_token + i as u32 + 1);688689			<PalletStructure<T>>::check_nesting(690				sender.clone(),691				&data.owner,692				collection.id,693				token,694				nesting_budget,695			)?;696		}697698		// =========699700		with_transaction(|| {701			for (i, data) in data.iter().enumerate() {702				let token = first_token + i as u32 + 1;703704				<TokenData<T>>::insert(705					(collection.id, token),706					ItemData {707						// const_data: data.const_data.clone(),708						owner: data.owner.clone(),709					},710				);711712				<PalletStructure<T>>::nest_if_sent_to_token_unchecked(&data.owner, collection.id, TokenId(token));713714				if let Err(e) = Self::set_token_properties(715					collection,716					sender,717					TokenId(token),718					data.properties.clone().into_inner(),719				) {720					return TransactionOutcome::Rollback(Err(e));721				}722			}723			TransactionOutcome::Commit(Ok(()))724		})?;725726		<TokensMinted<T>>::insert(collection.id, tokens_minted);727		for (account, balance) in balances {728			<AccountBalance<T>>::insert((collection.id, account), balance);729		}730		for (i, data) in data.into_iter().enumerate() {731			let token = first_token + i as u32 + 1;732			<Owned<T>>::insert((collection.id, &data.owner, token), true);733734			<PalletEvm<T>>::deposit_log(735				ERC721Events::Transfer {736					from: H160::default(),737					to: *data.owner.as_eth(),738					token_id: token.into(),739				}740				.to_log(collection_id_to_address(collection.id)),741			);742			<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(743				collection.id,744				TokenId(token),745				data.owner.clone(),746				1,747			));748		}749		Ok(())750	}751752	pub fn set_allowance_unchecked(753		collection: &NonfungibleHandle<T>,754		sender: &T::CrossAccountId,755		token: TokenId,756		spender: Option<&T::CrossAccountId>,757		assume_implicit_eth: bool,758	) {759		if let Some(spender) = spender {760			let old_spender = <Allowance<T>>::get((collection.id, token));761			<Allowance<T>>::insert((collection.id, token), spender);762			// In ERC721 there is only one possible approved user of token, so we set763			// approved user to spender764			<PalletEvm<T>>::deposit_log(765				ERC721Events::Approval {766					owner: *sender.as_eth(),767					approved: *spender.as_eth(),768					token_id: token.into(),769				}770				.to_log(collection_id_to_address(collection.id)),771			);772			// In Unique chain, any token can have any amount of approved users, so we need to773			// set allowance of old owner to 0, and allowance of new owner to 1774			if old_spender.as_ref() != Some(spender) {775				if let Some(old_owner) = old_spender {776					<PalletCommon<T>>::deposit_event(CommonEvent::Approved(777						collection.id,778						token,779						sender.clone(),780						old_owner,781						0,782					));783				}784				<PalletCommon<T>>::deposit_event(CommonEvent::Approved(785					collection.id,786					token,787					sender.clone(),788					spender.clone(),789					1,790				));791			}792		} else {793			let old_spender = <Allowance<T>>::take((collection.id, token));794			if !assume_implicit_eth {795				// In ERC721 there is only one possible approved user of token, so we set796				// approved user to zero address797				<PalletEvm<T>>::deposit_log(798					ERC721Events::Approval {799						owner: *sender.as_eth(),800						approved: H160::default(),801						token_id: token.into(),802					}803					.to_log(collection_id_to_address(collection.id)),804				);805			}806			// In Unique chain, any token can have any amount of approved users, so we need to807			// set allowance of old owner to 0808			if let Some(old_spender) = old_spender {809				<PalletCommon<T>>::deposit_event(CommonEvent::Approved(810					collection.id,811					token,812					sender.clone(),813					old_spender,814					0,815				));816			}817		}818	}819820	pub fn set_allowance(821		collection: &NonfungibleHandle<T>,822		sender: &T::CrossAccountId,823		token: TokenId,824		spender: Option<&T::CrossAccountId>,825	) -> DispatchResult {826		if collection.permissions.access() == AccessMode::AllowList {827			collection.check_allowlist(sender)?;828			if let Some(spender) = spender {829				collection.check_allowlist(spender)?;830			}831		}832833		if let Some(spender) = spender {834			<PalletCommon<T>>::ensure_correct_receiver(spender)?;835		}836		let token_data =837			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;838		if &token_data.owner != sender {839			ensure!(840				collection.ignores_owned_amount(sender),841				<CommonError<T>>::CantApproveMoreThanOwned842			);843		}844845		// =========846847		Self::set_allowance_unchecked(collection, sender, token, spender, false);848		Ok(())849	}850851	fn check_allowed(852		collection: &NonfungibleHandle<T>,853		spender: &T::CrossAccountId,854		from: &T::CrossAccountId,855		token: TokenId,856		nesting_budget: &dyn Budget,857	) -> DispatchResult {858		if spender.conv_eq(from) {859			return Ok(());860		}861		if collection.permissions.access() == AccessMode::AllowList {862			// `from`, `to` checked in [`transfer`]863			collection.check_allowlist(spender)?;864		}865		if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {866			// TODO: should collection owner be allowed to perform this transfer?867			ensure!(868				<PalletStructure<T>>::check_indirectly_owned(869					spender.clone(),870					source.0,871					source.1,872					None,873					nesting_budget874				)?,875				<CommonError<T>>::ApprovedValueTooLow,876			);877			return Ok(());878		}879		if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {880			return Ok(());881		}882		ensure!(883			collection.ignores_allowance(spender),884			<CommonError<T>>::ApprovedValueTooLow885		);886		Ok(())887	}888889	pub fn transfer_from(890		collection: &NonfungibleHandle<T>,891		spender: &T::CrossAccountId,892		from: &T::CrossAccountId,893		to: &T::CrossAccountId,894		token: TokenId,895		nesting_budget: &dyn Budget,896	) -> DispatchResult {897		Self::check_allowed(collection, spender, from, token, nesting_budget)?;898899		// =========900901		// Allowance is reset in [`transfer`]902		Self::transfer(collection, from, to, token, nesting_budget)903	}904905	pub fn burn_from(906		collection: &NonfungibleHandle<T>,907		spender: &T::CrossAccountId,908		from: &T::CrossAccountId,909		token: TokenId,910		nesting_budget: &dyn Budget,911	) -> DispatchResult {912		Self::check_allowed(collection, spender, from, token, nesting_budget)?;913914		// =========915916		Self::burn(collection, from, token)917	}918919	pub fn check_nesting(920		handle: &NonfungibleHandle<T>,921		sender: T::CrossAccountId,922		from: (CollectionId, TokenId),923		under: TokenId,924		nesting_budget: &dyn Budget,925	) -> DispatchResult {926		fn ensure_sender_allowed<T: Config>(927			collection: CollectionId,928			token: TokenId,929			for_nest: (CollectionId, TokenId),930			sender: T::CrossAccountId,931			budget: &dyn Budget,932		) -> DispatchResult {933			ensure!(934				<PalletStructure<T>>::check_indirectly_owned(935					sender,936					collection,937					token,938					Some(for_nest),939					budget940				)?,941				<CommonError<T>>::OnlyOwnerAllowedToNest,942			);943			Ok(())944		}945		match handle.permissions.nesting() {946			NestingRule::Disabled => fail!(<CommonError<T>>::NestingIsDisabled),947			NestingRule::Owner => {948				ensure_sender_allowed::<T>(handle.id, under, from, sender, nesting_budget)?949			}950			NestingRule::OwnerRestricted(whitelist) => {951				ensure!(952					whitelist.contains(&from.0),953					<CommonError<T>>::SourceCollectionIsNotAllowedToNest954				);955				ensure_sender_allowed::<T>(handle.id, under, from, sender, nesting_budget)?956			}957		}958		Ok(())959	}960961	fn nest(962		under: (CollectionId, TokenId),963		to_nest: (CollectionId, TokenId),964	) {965		<TokenChildren<T>>::insert(966			(under.0, under.1, (to_nest.0, to_nest.1)),967			true968		);969	}970971	fn unnest(972		under: (CollectionId, TokenId),973		to_unnest: (CollectionId, TokenId),974	) {975		<TokenChildren<T>>::remove(976			(under.0, under.1, to_unnest)977		);978	}979980	fn collection_has_tokens(collection_id: CollectionId) -> bool {981		<TokenData<T>>::iter_prefix((collection_id,)).next().is_some()982	}983984	fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool {985		<TokenChildren<T>>::iter_prefix((collection_id, token_id)).next().is_some()986	}987988	/// Delegated to `create_multiple_items`989	pub fn create_item(990		collection: &NonfungibleHandle<T>,991		sender: &T::CrossAccountId,992		data: CreateItemData<T>,993		nesting_budget: &dyn Budget,994	) -> DispatchResult {995		Self::create_multiple_items(collection, sender, vec![data], nesting_budget)996	}997}
modifiedpallets/proxy-rmrk-core/src/misc.rsdiffbeforeafterboth
--- a/pallets/proxy-rmrk-core/src/misc.rs
+++ b/pallets/proxy-rmrk-core/src/misc.rs
@@ -26,6 +26,18 @@
     }
 }
 
+pub trait RmrkRebind<T, S> {
+    fn rebind(&self) -> BoundedVec<u8, S>;
+}
+
+impl<T, S> RmrkRebind<T, S> for BoundedVec<u8, T> where BoundedVec<u8, S>: TryFrom<Vec<u8>> {
+    fn rebind(&self) -> BoundedVec<u8, S> {
+        BoundedVec::<u8, S>::try_from(
+            self.clone().into_inner()
+        ).unwrap_or_default()
+    }
+}
+
 #[derive(Encode, Decode, PartialEq, Eq)]
 pub enum CollectionType {
     Regular,
modifiedpallets/refungible/src/common.rsdiffbeforeafterboth
--- a/pallets/refungible/src/common.rs
+++ b/pallets/refungible/src/common.rs
@@ -313,6 +313,18 @@
 		fail!(<Error<T>>::RefungibleDisallowsNesting)
 	}
 
+	fn nest(
+		&self,
+		_under: TokenId,
+		_to_nest: (CollectionId, TokenId)
+	) {}
+
+	fn unnest(
+		&self,
+		_under: TokenId,
+		_to_nest: (CollectionId, TokenId)
+	) {}
+
 	fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId> {
 		<Owned<T>>::iter_prefix((self.id, account))
 			.map(|(id, _)| id)
modifiedpallets/refungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -23,8 +23,7 @@
 };
 use pallet_evm::account::CrossAccountId;
 use pallet_common::{
-	Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, CollectionHandle,
-	dispatch::CollectionDispatch,
+	Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,
 };
 use pallet_structure::Pallet as PalletStructure;
 use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};
@@ -211,6 +210,10 @@
 	) -> DispatchResult {
 		let id = collection.id;
 
+		if Self::collection_has_tokens(id) {
+			return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());
+		}
+
 		// =========
 
 		PalletCommon::destroy_collection(collection.0, sender)?;
@@ -226,6 +229,10 @@
 		Ok(())
 	}
 
+	fn collection_has_tokens(collection_id: CollectionId) -> bool {
+		<TokenData<T>>::iter_prefix((collection_id,)).next().is_some()
+	}
+
 	pub fn burn_token(collection: &RefungibleHandle<T>, token_id: TokenId) -> DispatchResult {
 		let burnt = <TokensBurnt<T>>::get(collection.id)
 			.checked_add(1)
@@ -265,6 +272,7 @@
 			// =========
 
 			<Owned<T>>::remove((collection.id, owner, token));
+			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);
 			<AccountBalance<T>>::insert((collection.id, owner), account_balance);
 			Self::burn_token(collection, token)?;
 			<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(
@@ -292,6 +300,7 @@
 
 		if balance == 0 {
 			<Owned<T>>::remove((collection.id, owner, token));
+			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);
 			<Balance<T>>::remove((collection.id, token, owner));
 			<AccountBalance<T>>::insert((collection.id, owner), account_balance);
 		} else {
@@ -372,25 +381,25 @@
 			None
 		};
 
-		if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
-			let handle = <CollectionHandle<T>>::try_get(target.0)?;
-			let dispatch = T::CollectionDispatch::dispatch(handle);
-			let dispatch = dispatch.as_dyn();
+		// =========
 
-			dispatch.check_nesting(
-				from.clone(),
-				(collection.id, token),
-				target.1,
-				nesting_budget,
-			)?;
-		}
-
-		// =========
+		<PalletStructure<T>>::nest_if_sent_to_token(
+			from.clone(),
+			to,
+			collection.id,
+			token,
+			nesting_budget
+		)?;
 
 		if let Some(balance_to) = balance_to {
 			// from != to
 			if balance_from == 0 {
 				<Balance<T>>::remove((collection.id, token, from));
+				<PalletStructure<T>>::unnest_if_nested(
+					from,
+					collection.id,
+					token
+				);
 			} else {
 				<Balance<T>>::insert((collection.id, token, from), balance_from);
 			}
@@ -488,18 +497,14 @@
 		for (i, token) in data.iter().enumerate() {
 			let token_id = TokenId(first_token_id + i as u32 + 1);
 			for (to, _) in token.users.iter() {
-				if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
-					let handle = <CollectionHandle<T>>::try_get(target.0)?;
-					let dispatch = T::CollectionDispatch::dispatch(handle);
-					let dispatch = dispatch.as_dyn();
 
-					dispatch.check_nesting(
-						sender.clone(),
-						(collection.id, token_id),
-						target.1,
-						nesting_budget,
-					)?;
-				}
+				<PalletStructure<T>>::check_nesting(
+					sender.clone(),
+					to,
+					collection.id,
+					token_id,
+					nesting_budget,
+				)?;
 			}
 		}
 
@@ -519,12 +524,15 @@
 					const_data: token.const_data,
 				},
 			);
+
 			for (user, amount) in token.users.into_iter() {
 				if amount == 0 {
 					continue;
 				}
 				<Balance<T>>::insert((collection.id, token_id, &user), amount);
 				<Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);
+				<PalletStructure<T>>::nest_if_sent_to_token_unchecked(&user, collection.id, TokenId(token_id));
+
 				// TODO: ERC20 transfer event
 				<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(
 					collection.id,
modifiedpallets/structure/src/lib.rsdiffbeforeafterboth
--- a/pallets/structure/src/lib.rs
+++ b/pallets/structure/src/lib.rs
@@ -1,8 +1,9 @@
 #![cfg_attr(not(feature = "std"), no_std)]
 
+use pallet_common::CommonCollectionOperations;
 use sp_std::collections::btree_set::BTreeSet;
 
-use frame_support::dispatch::DispatchError;
+use frame_support::dispatch::{DispatchError, DispatchResult};
 use frame_support::fail;
 pub use pallet::*;
 use pallet_common::{dispatch::CollectionDispatch, CollectionHandle};
@@ -155,8 +156,8 @@
 		budget: &dyn Budget,
 	) -> Result<bool, DispatchError> {
 		let target_parent = match T::CrossTokenAddressMapping::address_to_token(&user) {
-			Some((collection, token)) => Parent::Token(collection, token),
-			None => Parent::User(user),
+			Some((collection, token)) => Self::find_topmost_owner(collection, token, budget)?,
+			None => user,
 		};
 
 		// Tried to nest token in itself
@@ -171,10 +172,10 @@
 					return Err(<Error<T>>::OuroborosDetected.into())
 				}
 				// Found needed parent, token is indirecty owned
-				v if v == target_parent => return Ok(true),
+				Parent::User(user) if user == target_parent => return Ok(true),
 				// Token is owned by other user
 				Parent::User(_) => return Ok(false),
-				Parent::TokenNotFound => return Ok(false),
+				Parent::TokenNotFound => return Err(<Error<T>>::TokenNotFound.into()),
 				// Continue parent chain
 				Parent::Token(_, _) => {}
 			}
@@ -182,4 +183,113 @@
 
 		Err(<Error<T>>::DepthLimit.into())
 	}
+
+	pub fn check_nesting(
+		from: T::CrossAccountId,
+		under: &T::CrossAccountId,
+		collection_id: CollectionId,
+		token_id: TokenId,
+		nesting_budget: &dyn Budget
+	) -> DispatchResult {
+		Self::try_exec_if_owner_is_valid_nft(
+			under,
+			|d, parent_id| d.check_nesting(
+				from,
+				(collection_id, token_id),
+				parent_id,
+				nesting_budget
+			)
+		)
+	}
+
+	pub fn nest_if_sent_to_token(
+		from: T::CrossAccountId,
+		under: &T::CrossAccountId,
+		collection_id: CollectionId,
+		token_id: TokenId,
+		nesting_budget: &dyn Budget
+	) -> DispatchResult {
+		Self::try_exec_if_owner_is_valid_nft(
+			under,
+			|d, parent_id| {
+				d.check_nesting(
+					from,
+					(collection_id, token_id),
+					parent_id,
+					nesting_budget
+				)?;
+
+				d.nest(parent_id, (collection_id, token_id));
+
+				Ok(())
+			}
+		)
+	}
+
+	pub fn nest_if_sent_to_token_unchecked(
+		owner: &T::CrossAccountId,
+		collection_id: CollectionId,
+		token_id: TokenId
+	) {
+		Self::exec_if_owner_is_valid_nft(
+			owner,
+			|d, parent_id| d.nest(
+				parent_id,
+				(collection_id, token_id)
+			)
+		);
+	}
+
+	pub fn unnest_if_nested(
+		owner: &T::CrossAccountId,
+		collection_id: CollectionId,
+		token_id: TokenId
+	) {
+		Self::exec_if_owner_is_valid_nft(
+			owner,
+			|d, parent_id| d.unnest(
+			parent_id,
+			(collection_id, token_id)
+			)
+		);
+	}
+
+	fn exec_if_owner_is_valid_nft(
+		account: &T::CrossAccountId,
+		action: impl FnOnce(&dyn CommonCollectionOperations<T>, TokenId)
+	) {
+		Self::try_exec_if_owner_is_valid_nft(
+			account,
+			|d, id| {
+				action(d, id);
+				Ok(())
+			}
+		).unwrap();
+	}
+
+	fn try_exec_if_owner_is_valid_nft(
+		account: &T::CrossAccountId,
+		action: impl FnOnce(&dyn CommonCollectionOperations<T>, TokenId) -> DispatchResult
+	) -> DispatchResult {
+		let account = T::CrossTokenAddressMapping::address_to_token(account);
+
+		if account.is_none() {
+			return Ok(());
+		}
+
+		let account = account.unwrap();
+
+		let handle = <CollectionHandle<T>>::try_get(account.0);
+
+		if handle.is_err() {
+			return Ok(());
+		}
+
+		let handle = handle.unwrap();
+
+		let dispatch = T::CollectionDispatch::dispatch(handle);
+		let dispatch = dispatch.as_dyn();
+
+		action(dispatch, account.1)
+	}
 }
modifiedpallets/unique/src/lib.rsdiffbeforeafterboth
--- a/pallets/unique/src/lib.rs
+++ b/pallets/unique/src/lib.rs
@@ -55,6 +55,8 @@
 pub mod weights;
 use weights::WeightInfo;
 
+const NESTING_BUDGET: u32 = 5;
+
 decl_error! {
 	/// Error for non-fungible-token module.
 	pub enum Error for Module<T: Config> {
@@ -569,7 +571,7 @@
 		#[transactional]
 		pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResultWithPostInfo {
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
-			let budget = budget::Value::new(2);
+			let budget = budget::Value::new(NESTING_BUDGET);
 
 			dispatch_call::<T, _>(collection_id, |d| d.create_item(sender, owner, data, &budget))
 		}
@@ -597,7 +599,7 @@
 		pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec<CreateItemData>) -> DispatchResultWithPostInfo {
 			ensure!(!items_data.is_empty(), Error::<T>::EmptyArgument);
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
-			let budget = budget::Value::new(2);
+			let budget = budget::Value::new(NESTING_BUDGET);
 
 			dispatch_call::<T, _>(collection_id, |d| d.create_multiple_items(sender, owner, items_data, &budget))
 		}
@@ -678,7 +680,7 @@
 		#[transactional]
 		pub fn create_multiple_items_ex(origin, collection_id: CollectionId, data: CreateItemExData<T::CrossAccountId>) -> DispatchResultWithPostInfo {
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
-			let budget = budget::Value::new(2);
+			let budget = budget::Value::new(NESTING_BUDGET);
 
 			dispatch_call::<T, _>(collection_id, |d| d.create_multiple_items_ex(sender, data, &budget))
 		}
@@ -758,7 +760,7 @@
 		#[transactional]
 		pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
-			let budget = budget::Value::new(2);
+			let budget = budget::Value::new(NESTING_BUDGET);
 
 			dispatch_call::<T, _>(collection_id, |d| d.burn_from(sender, from, item_id, value, &budget))
 		}
@@ -790,7 +792,7 @@
 		#[transactional]
 		pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
-			let budget = budget::Value::new(2);
+			let budget = budget::Value::new(NESTING_BUDGET);
 
 			dispatch_call::<T, _>(collection_id, |d| d.transfer(sender, recipient, item_id, value, &budget))
 		}
@@ -841,7 +843,7 @@
 		#[transactional]
 		pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResultWithPostInfo {
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
-			let budget = budget::Value::new(2);
+			let budget = budget::Value::new(NESTING_BUDGET);
 
 			dispatch_call::<T, _>(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value, &budget))
 		}
modifiedruntime/common/src/runtime_apis.rsdiffbeforeafterboth
--- a/runtime/common/src/runtime_apis.rs
+++ b/runtime/common/src/runtime_apis.rs
@@ -25,7 +25,7 @@
                     dispatch_unique_runtime!(collection.token_owner(token))
                 }
                 fn topmost_token_owner(collection: CollectionId, token: TokenId) -> Result<Option<CrossAccountId>, DispatchError> {
-                    let budget = up_data_structs::budget::Value::new(5);
+                    let budget = up_data_structs::budget::Value::new(10);
 
                     Ok(Some(<pallet_structure::Pallet<Runtime>>::find_topmost_owner(collection, token, &budget)?))
                 }
@@ -142,7 +142,7 @@
                 }
 
                 fn collection_by_id(collection_id: RmrkCollectionId) -> Result<Option<RmrkCollectionInfo<AccountId>>, DispatchError> {
-                    use pallet_proxy_rmrk_core::{RmrkProperty, misc::{CollectionType, RmrkDecode}};
+                    use pallet_proxy_rmrk_core::{RmrkProperty, misc::{CollectionType, RmrkDecode, RmrkRebind}};
 
                     let collection_id = CollectionId(collection_id);
                     let collection = match RmrkCore::get_typed_nft_collection(collection_id, CollectionType::Regular) {
@@ -156,7 +156,7 @@
                         issuer: collection.owner.clone(),
                         metadata: RmrkCore::get_collection_property(collection_id, RmrkProperty::Metadata)?.decode_or_default(),
                         max: collection.limits.token_limit,
-                        symbol: collection.token_prefix.decode_or_default(),
+                        symbol: collection.token_prefix.rebind(),
                         nfts_count
                     }))
                 }
@@ -204,22 +204,21 @@
                 }
 
                 fn nft_children(collection_id: RmrkCollectionId, nft_id: RmrkNftId) -> Result<Vec<RmrkNftChild>, DispatchError> {
-                    use up_data_structs::mapping::TokenAddressMapping;
-
                     let collection_id = CollectionId(collection_id);
                     let nft_id = TokenId(nft_id);
                     if !RmrkCore::nft_exists(collection_id, nft_id) { return Ok(Vec::new()); }
 
-                    let cross_account_id = CrossAccountId::from_eth(
-                        EvmTokenAddressMapping::token_to_address(collection_id, nft_id)
-                    );
-
                     Ok(
-                        pallet_nonfungible::Owned::<Runtime>::iter_prefix((collection_id, cross_account_id))
-                            .map(|(child_id, _)| RmrkNftChild {
-                                collection_id: collection_id.0, // todo make sure they're always from this collection // spoiler: they're not
-                                nft_id: child_id.0,
-                            }).collect()
+                        pallet_nonfungible::TokenChildren::<Runtime>::iter_prefix((collection_id, nft_id))
+                            .filter_map(|(child_id, is_child)|
+                                match is_child {
+                                    true => Some(RmrkNftChild {
+                                        collection_id: child_id.0.0,
+                                        nft_id: child_id.1.0,
+                                    }),
+                                    false => None,
+                                }
+                            ).collect()
                     )
                 }
 
@@ -332,7 +331,7 @@
 
                 fn base(base_id: RmrkBaseId) -> Result<Option<RmrkBaseInfo<AccountId>>, DispatchError> {
                     use pallet_proxy_rmrk_core::{
-                        RmrkProperty, misc::{CollectionType, RmrkDecode},
+                        RmrkProperty, misc::{CollectionType, RmrkDecode, RmrkRebind},
                     };
 
                     let collection_id = CollectionId(base_id);
@@ -344,7 +343,7 @@
                     Ok(Some(RmrkBaseInfo {
                         issuer: collection.owner.clone(),
                         base_type: RmrkCore::get_collection_property(collection_id, RmrkProperty::BaseType)?.decode_or_default(),
-                        symbol: collection.token_prefix.decode_or_default(),
+                        symbol: collection.token_prefix.rebind(),
                     }))
                 }
 
modifiedtests/src/nesting/nest.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/nest.test.ts
+++ b/tests/src/nesting/nest.test.ts
@@ -41,7 +41,7 @@
 
       // Create a token to be nested
       const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
-      
+
       // Nest
       await transferExpectSuccess(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)});
       expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
@@ -111,8 +111,8 @@
 
       // Create a nested token
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT, 
-        targetAddress, 
+        collectionFT,
+        targetAddress,
         {Fungible: {Value: 10}},
       ))).to.not.be.rejected;
 
@@ -134,8 +134,8 @@
 
       // Create a nested token
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT, 
-        targetAddress, 
+        collectionFT,
+        targetAddress,
         {Fungible: {Value: 10}},
       ))).to.not.be.rejected;
 
@@ -158,8 +158,8 @@
 
       // Create a nested token
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT, 
-        targetAddress, 
+        collectionRFT,
+        targetAddress,
         {ReFungible: {const_data: [], pieces: 100}},
       ))).to.not.be.rejected;
 
@@ -181,7 +181,7 @@
 
       // Create a nested token
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT, 
+        collectionRFT,
         targetAddress,
         {ReFungible: {const_data: [], pieces: 100}},
       ))).to.not.be.rejected;
@@ -207,17 +207,29 @@
       await setCollectionPermissionsExceptSuccess(alice, collection, {nesting: 'Owner'});
       const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
 
+      const maxNestingLevel = 5;
+      let prevToken = targetToken;
+
       // Create a nested-token matryoshka
-      const nestedToken1 = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
-      const nestedToken2 = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, nestedToken1)});
-      // The nesting depth is limited by 2
+      for (let i = 0; i < maxNestingLevel; i++) {
+        const nestedToken = await createItemExpectSuccess(
+          alice,
+          collection,
+          'NFT',
+          {Ethereum: tokenIdToAddress(collection, prevToken)},
+        );
+
+        prevToken = nestedToken;
+      }
+
+      // The nesting depth is limited by `maxNestingLevel`
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collection, 
-        {Ethereum: tokenIdToAddress(collection, nestedToken2)}, 
+        collection,
+        {Ethereum: tokenIdToAddress(collection, prevToken)},
           {nft: {const_data: [], variable_data: []}} as any,
       )), 'while creating nested token').to.be.rejectedWith(/^structure\.DepthLimit$/);
 
-      expect(await getTopmostTokenOwner(api, collection, nestedToken2)).to.be.deep.equal({Substrate: alice.address});
+      expect(await getTopmostTokenOwner(api, collection, prevToken)).to.be.deep.equal({Substrate: alice.address});
     });
   });
 
@@ -231,8 +243,8 @@
 
       // Try to create a nested token
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collection, 
-        {Ethereum: tokenIdToAddress(collection, targetToken)}, 
+        collection,
+        {Ethereum: tokenIdToAddress(collection, targetToken)},
           {nft: {const_data: [], variable_data: []}} as any,
       )), 'while creating nested token').to.be.rejectedWith(/^common\.NestingIsDisabled$/);
 
@@ -259,8 +271,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collection, 
-        {Ethereum: tokenIdToAddress(collection, targetToken)}, 
+        collection,
+        {Ethereum: tokenIdToAddress(collection, targetToken)},
           {nft: {const_data: [], variable_data: []}} as any,
       )), 'while creating nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
 
@@ -285,8 +297,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collection, 
-        {Ethereum: tokenIdToAddress(collection, targetToken)}, 
+        collection,
+        {Ethereum: tokenIdToAddress(collection, targetToken)},
           {nft: {const_data: [], variable_data: []}} as any,
       )), 'while creating nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
 
@@ -307,8 +319,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collection, 
-        {Ethereum: tokenIdToAddress(collection, targetToken)}, 
+        collection,
+        {Ethereum: tokenIdToAddress(collection, targetToken)},
           {nft: {const_data: [], variable_data: []}} as any,
       )), 'while creating nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
 
@@ -332,11 +344,11 @@
 
       // Try to create a nested token
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT, 
-        targetAddress, 
+        collectionFT,
+        targetAddress,
         {Fungible: {Value: 10}},
       )), 'while creating nested token').to.be.rejectedWith(/^common\.NestingIsDisabled$/);
-      
+
       // Create a token to be nested
       const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
       // Try to nest
@@ -366,8 +378,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT, 
-        targetAddress, 
+        collectionFT,
+        targetAddress,
         {Fungible: {Value: 10}},
       )), 'while creating nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
 
@@ -393,8 +405,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT, 
-        targetAddress, 
+        collectionFT,
+        targetAddress,
         {Fungible: {Value: 10}},
       )), 'while creating nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
 
@@ -417,8 +429,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT, 
-        targetAddress, 
+        collectionFT,
+        targetAddress,
         {Fungible: {Value: 10}},
       )), 'while creating a nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
 
@@ -441,8 +453,8 @@
 
       // Create a nested token
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT, 
-        targetAddress, 
+        collectionRFT,
+        targetAddress,
         {ReFungible: {const_data: [], pieces: 100}},
       )), 'while creating a nested token').to.be.rejectedWith(/^common\.NestingIsDisabled$/);
 
@@ -477,8 +489,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT, 
-        targetAddress, 
+        collectionRFT,
+        targetAddress,
         {ReFungible: {const_data: [], pieces: 100}},
       )), 'while creating a nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
 
@@ -504,8 +516,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT, 
-        targetAddress, 
+        collectionRFT,
+        targetAddress,
         {ReFungible: {const_data: [], pieces: 100}},
       )), 'while creating a nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
 
@@ -528,8 +540,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT, 
-        targetAddress, 
+        collectionRFT,
+        targetAddress,
         {ReFungible: {const_data: [], pieces: 100}},
       )), 'while creating a nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);