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

difftreelog

CORE-390 Add read only flag

Trubnikov Sergey2022-06-03parent: #1009216.patch.diff
in: master

8 files changed

modifiedpallets/common/src/erc.rsdiffbeforeafterboth
--- a/pallets/common/src/erc.rs
+++ b/pallets/common/src/erc.rs
@@ -87,18 +87,16 @@
 		check_is_owner(caller, self)?;
 
 		let sponsor = T::CrossAccountId::from_eth(sponsor);
-		self.set_sponsor(sponsor.as_sub().clone());
-		save(self);
-		Ok(())
+		self.set_sponsor(sponsor.as_sub().clone()).map_err(dispatch_to_evm::<T>)?;
+		save(self)
 	}
 
 	fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result<void> {
 		let caller = T::CrossAccountId::from_eth(caller);
-		if !self.confirm_sponsorship(caller.as_sub()) {
+		if !self.confirm_sponsorship(caller.as_sub()).map_err(dispatch_to_evm::<T>)? {
 			return Err(Error::Revert("Caller is not set as sponsor".into()));
 		}
-		save(self);
-		Ok(())
+		save(self)
 	}
 
 	#[solidity(rename_selector = "setCollectionLimit")]
@@ -134,8 +132,7 @@
 		}
 		self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)
 			.map_err(dispatch_to_evm::<T>)?;
-		save(self);
-		Ok(())
+		save(self)
 	}
 
 	#[solidity(rename_selector = "setCollectionLimit")]
@@ -162,8 +159,7 @@
 		}
 		self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)
 			.map_err(dispatch_to_evm::<T>)?;
-		save(self);
-		Ok(())
+		save(self)
 	}
 
 	fn contract_address(&self, _caller: caller) -> Result<address> {
@@ -296,7 +292,7 @@
 	}
 }
 
-fn check_is_owner<T: Config>(caller: caller, collection: &CollectionHandle<T>) -> Result<()> {
+fn check_is_owner<T: Config>(caller: caller, collection: &CollectionHandle<T>) -> Result<void> {
 	let caller = T::CrossAccountId::from_eth(caller);
 	collection
 		.check_is_owner(&caller)
@@ -315,8 +311,10 @@
 	Ok(caller)
 }
 
-fn save<T: Config>(collection: &CollectionHandle<T>) {
+fn save<T: Config>(collection: &CollectionHandle<T>) -> Result<void> {
+	collection.check_is_read_only().map_err(dispatch_to_evm::<T>)?;
 	<crate::CollectionById<T>>::insert(collection.id, collection.collection.clone());
+	Ok(())
 }
 
 pub fn token_uri_key() -> up_data_structs::PropertyKey {
modifiedpallets/common/src/lib.rsdiffbeforeafterboth
before · pallets/common/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)]1819extern crate alloc;2021use core::ops::{Deref, DerefMut};22use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};23use sp_std::vec::Vec;24use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};25use evm_coder::ToLog;26use frame_support::{27	dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, Weight, PostDispatchInfo},28	ensure,29	traits::{Imbalance, Get, Currency, WithdrawReasons, ExistenceRequirement},30	weights::Pays,31	transactional,32};33use pallet_evm::GasWeightMapping;34use up_data_structs::{35	COLLECTION_NUMBER_LIMIT,36	Collection,37	RpcCollection,38	CollectionId,39	CreateItemData,40	MAX_TOKEN_PREFIX_LENGTH,41	COLLECTION_ADMINS_LIMIT,42	TokenId,43	TokenChild,44	CollectionStats,45	MAX_TOKEN_OWNERSHIP,46	CollectionMode,47	NFT_SPONSOR_TRANSFER_TIMEOUT,48	FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,49	REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,50	MAX_SPONSOR_TIMEOUT,51	CUSTOM_DATA_LIMIT,52	CollectionLimits,53	CreateCollectionData,54	SponsorshipState,55	CreateItemExData,56	SponsoringRateLimit,57	budget::Budget,58	PhantomType,59	Property,60	Properties,61	PropertiesPermissionMap,62	PropertyKey,63	PropertyValue,64	PropertyPermission,65	PropertiesError,66	PropertyKeyPermission,67	TokenData,68	TrySetProperty,69	PropertyScope,70	// RMRK71	RmrkCollectionInfo,72	RmrkInstanceInfo,73	RmrkResourceInfo,74	RmrkPropertyInfo,75	RmrkBaseInfo,76	RmrkPartType,77	RmrkTheme,78	RmrkNftChild,79	CollectionPermissions,80	SchemaVersion,81};8283pub use pallet::*;84use sp_core::H160;85use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};86#[cfg(feature = "runtime-benchmarks")]87pub mod benchmarking;88pub mod dispatch;89pub mod erc;90pub mod eth;91pub mod weights;9293pub type SelfWeightOf<T> = <T as Config>::WeightInfo;9495#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]96pub struct CollectionHandle<T: Config> {97	pub id: CollectionId,98	collection: Collection<T::AccountId>,99	pub recorder: SubstrateRecorder<T>,100}101impl<T: Config> WithRecorder<T> for CollectionHandle<T> {102	fn recorder(&self) -> &SubstrateRecorder<T> {103		&self.recorder104	}105	fn into_recorder(self) -> SubstrateRecorder<T> {106		self.recorder107	}108}109impl<T: Config> CollectionHandle<T> {110	pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {111		<CollectionById<T>>::get(id).map(|collection| Self {112			id,113			collection,114			recorder: SubstrateRecorder::new(gas_limit),115		})116	}117118	pub fn new_with_recorder(id: CollectionId, recorder: SubstrateRecorder<T>) -> Option<Self> {119		<CollectionById<T>>::get(id).map(|collection| Self {120			id,121			collection,122			recorder,123		})124	}125126	pub fn new(id: CollectionId) -> Option<Self> {127		Self::new_with_gas_limit(id, u64::MAX)128	}129130	pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {131		Ok(Self::new(id).ok_or(<Error<T>>::CollectionNotFound)?)132	}133134	pub fn consume_store_reads(&self, reads: u64) -> evm_coder::execution::Result<()> {135		self.recorder136			.consume_gas(T::GasWeightMapping::weight_to_gas(137				<T as frame_system::Config>::DbWeight::get()138					.read139					.saturating_mul(reads),140			))141	}142143	pub fn consume_store_writes(&self, writes: u64) -> evm_coder::execution::Result<()> {144		self.recorder145			.consume_gas(T::GasWeightMapping::weight_to_gas(146				<T as frame_system::Config>::DbWeight::get()147					.write148					.saturating_mul(writes),149			))150	}151152	pub fn save(self) -> DispatchResult {153		<CollectionById<T>>::insert(self.id, self.collection);154		Ok(())155	}156157	pub fn set_sponsor(&mut self, sponsor: T::AccountId) {158		self.collection.sponsorship = SponsorshipState::Unconfirmed(sponsor);159	}160161	pub fn confirm_sponsorship(&mut self, sender: &T::AccountId) -> bool {162		if self.collection.sponsorship.pending_sponsor() != Some(sender) {163			return false;164		};165166		self.collection.sponsorship = SponsorshipState::Confirmed(sender.clone());167		true168	}169}170171impl<T: Config> Deref for CollectionHandle<T> {172	type Target = Collection<T::AccountId>;173174	fn deref(&self) -> &Self::Target {175		&self.collection176	}177}178179impl<T: Config> DerefMut for CollectionHandle<T> {180	fn deref_mut(&mut self) -> &mut Self::Target {181		&mut self.collection182	}183}184185impl<T: Config> CollectionHandle<T> {186	pub fn check_is_owner(&self, subject: &T::CrossAccountId) -> DispatchResult {187		ensure!(*subject.as_sub() == self.owner, <Error<T>>::NoPermission);188		Ok(())189	}190	pub fn is_owner_or_admin(&self, subject: &T::CrossAccountId) -> bool {191		*subject.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, subject))192	}193	pub fn check_is_owner_or_admin(&self, subject: &T::CrossAccountId) -> DispatchResult {194		ensure!(self.is_owner_or_admin(subject), <Error<T>>::NoPermission);195		Ok(())196	}197	pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool {198		self.limits.owner_can_transfer() && self.is_owner_or_admin(user)199	}200	pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool {201		self.limits.owner_can_transfer() && self.is_owner_or_admin(user)202	}203	pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {204		ensure!(205			<Allowlist<T>>::get((self.id, user)),206			<Error<T>>::AddressNotInAllowlist207		);208		Ok(())209	}210}211212#[frame_support::pallet]213pub mod pallet {214	use super::*;215	use pallet_evm::account;216	use dispatch::CollectionDispatch;217	use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key, traits::StorageVersion};218	use frame_system::pallet_prelude::*;219	use frame_support::traits::Currency;220	use up_data_structs::{TokenId, mapping::TokenAddressMapping};221	use scale_info::TypeInfo;222	use weights::WeightInfo;223224	#[pallet::config]225	pub trait Config:226		frame_system::Config227		+ pallet_evm_coder_substrate::Config228		+ pallet_evm::Config229		+ TypeInfo230		+ account::Config231	{232		type WeightInfo: WeightInfo;233		type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;234235		type Currency: Currency<Self::AccountId>;236237		#[pallet::constant]238		type CollectionCreationPrice: Get<239			<<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,240		>;241		type CollectionDispatch: CollectionDispatch<Self>;242243		type TreasuryAccountId: Get<Self::AccountId>;244		type ContractAddress: Get<H160>;245246		type EvmTokenAddressMapping: TokenAddressMapping<H160>;247		type CrossTokenAddressMapping: TokenAddressMapping<Self::CrossAccountId>;248	}249250	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);251252	#[pallet::pallet]253	#[pallet::storage_version(STORAGE_VERSION)]254	#[pallet::generate_store(pub(super) trait Store)]255	pub struct Pallet<T>(_);256257	#[pallet::extra_constants]258	impl<T: Config> Pallet<T> {259		pub fn collection_admins_limit() -> u32 {260			COLLECTION_ADMINS_LIMIT261		}262	}263264	#[pallet::event]265	#[pallet::generate_deposit(pub fn deposit_event)]266	pub enum Event<T: Config> {267		/// New collection was created268		///269		/// # Arguments270		///271		/// * collection_id: Globally unique identifier of newly created collection.272		///273		/// * mode: [CollectionMode] converted into u8.274		///275		/// * account_id: Collection owner.276		CollectionCreated(CollectionId, u8, T::AccountId),277278		/// New collection was destroyed279		///280		/// # Arguments281		///282		/// * collection_id: Globally unique identifier of collection.283		CollectionDestroyed(CollectionId),284285		/// New item was created.286		///287		/// # Arguments288		///289		/// * collection_id: Id of the collection where item was created.290		///291		/// * item_id: Id of an item. Unique within the collection.292		///293		/// * recipient: Owner of newly created item294		///295		/// * amount: Always 1 for NFT296		ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128),297298		/// Collection item was burned.299		///300		/// # Arguments301		///302		/// * collection_id.303		///304		/// * item_id: Identifier of burned NFT.305		///306		/// * owner: which user has destroyed its tokens307		///308		/// * amount: Always 1 for NFT309		ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128),310311		/// Item was transferred312		///313		/// * collection_id: Id of collection to which item is belong314		///315		/// * item_id: Id of an item316		///317		/// * sender: Original owner of item318		///319		/// * recipient: New owner of item320		///321		/// * amount: Always 1 for NFT322		Transfer(323			CollectionId,324			TokenId,325			T::CrossAccountId,326			T::CrossAccountId,327			u128,328		),329330		/// * collection_id331		///332		/// * item_id333		///334		/// * sender335		///336		/// * spender337		///338		/// * amount339		Approved(340			CollectionId,341			TokenId,342			T::CrossAccountId,343			T::CrossAccountId,344			u128,345		),346347		CollectionPropertySet(CollectionId, PropertyKey),348349		CollectionPropertyDeleted(CollectionId, PropertyKey),350351		TokenPropertySet(CollectionId, TokenId, PropertyKey),352353		TokenPropertyDeleted(CollectionId, TokenId, PropertyKey),354355		PropertyPermissionSet(CollectionId, PropertyKey),356	}357358	#[pallet::error]359	pub enum Error<T> {360		/// This collection does not exist.361		CollectionNotFound,362		/// Sender parameter and item owner must be equal.363		MustBeTokenOwner,364		/// No permission to perform action365		NoPermission,366		/// Destroying only empty collections is allowed367		CantDestroyNotEmptyCollection,368		/// Collection is not in mint mode.369		PublicMintingNotAllowed,370		/// Address is not in allow list.371		AddressNotInAllowlist,372373		/// Collection name can not be longer than 63 char.374		CollectionNameLimitExceeded,375		/// Collection description can not be longer than 255 char.376		CollectionDescriptionLimitExceeded,377		/// Token prefix can not be longer than 15 char.378		CollectionTokenPrefixLimitExceeded,379		/// Total collections bound exceeded.380		TotalCollectionsLimitExceeded,381		/// Exceeded max admin count382		CollectionAdminCountExceeded,383		/// Collection limit bounds per collection exceeded384		CollectionLimitBoundsExceeded,385		/// Tried to enable permissions which are only permitted to be disabled386		OwnerPermissionsCantBeReverted,387		/// Collection settings not allowing items transferring388		TransferNotAllowed,389		/// Account token limit exceeded per collection390		AccountTokenLimitExceeded,391		/// Collection token limit exceeded392		CollectionTokenLimitExceeded,393		/// Metadata flag frozen394		MetadataFlagFrozen,395396		/// Item not exists.397		TokenNotFound,398		/// Item balance not enough.399		TokenValueTooLow,400		/// Requested value more than approved.401		ApprovedValueTooLow,402		/// Tried to approve more than owned403		CantApproveMoreThanOwned,404405		/// Can't transfer tokens to ethereum zero address406		AddressIsZero,407		/// Target collection doesn't supports this operation408		UnsupportedOperation,409410		/// Not sufficient founds to perform action411		NotSufficientFounds,412413		/// Collection has nesting disabled414		NestingIsDisabled,415		/// Only owner may nest tokens under this collection416		OnlyOwnerAllowedToNest,417		/// Only tokens from specific collections may nest tokens under this418		SourceCollectionIsNotAllowedToNest,419420		/// Tried to store more data than allowed in collection field421		CollectionFieldSizeExceeded,422423		/// Tried to store more property data than allowed424		NoSpaceForProperty,425426		/// Tried to store more property keys than allowed427		PropertyLimitReached,428429		/// Property key is too long430		PropertyKeyIsTooLong,431432		/// Only ASCII letters, digits, and '_', '-' are allowed433		InvalidCharacterInPropertyKey,434435		/// Empty property keys are forbidden436		EmptyPropertyKey,437	}438439	#[pallet::storage]440	pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;441	#[pallet::storage]442	pub type DestroyedCollectionCount<T> =443		StorageValue<Value = CollectionId, QueryKind = ValueQuery>;444445	/// Collection info446	#[pallet::storage]447	pub type CollectionById<T> = StorageMap<448		Hasher = Blake2_128Concat,449		Key = CollectionId,450		Value = Collection<<T as frame_system::Config>::AccountId>,451		QueryKind = OptionQuery,452	>;453454	/// Collection properties455	#[pallet::storage]456	#[pallet::getter(fn collection_properties)]457	pub type CollectionProperties<T> = StorageMap<458		Hasher = Blake2_128Concat,459		Key = CollectionId,460		Value = Properties,461		QueryKind = ValueQuery,462		OnEmpty = up_data_structs::CollectionProperties,463	>;464465	#[pallet::storage]466	#[pallet::getter(fn property_permissions)]467	pub type CollectionPropertyPermissions<T> = StorageMap<468		Hasher = Blake2_128Concat,469		Key = CollectionId,470		Value = PropertiesPermissionMap,471		QueryKind = ValueQuery,472	>;473474	#[pallet::storage]475	pub type AdminAmount<T> = StorageMap<476		Hasher = Blake2_128Concat,477		Key = CollectionId,478		Value = u32,479		QueryKind = ValueQuery,480	>;481482	/// List of collection admins483	#[pallet::storage]484	pub type IsAdmin<T: Config> = StorageNMap<485		Key = (486			Key<Blake2_128Concat, CollectionId>,487			Key<Blake2_128Concat, T::CrossAccountId>,488		),489		Value = bool,490		QueryKind = ValueQuery,491	>;492493	/// Allowlisted collection users494	#[pallet::storage]495	pub type Allowlist<T: Config> = StorageNMap<496		Key = (497			Key<Blake2_128Concat, CollectionId>,498			Key<Blake2_128Concat, T::CrossAccountId>,499		),500		Value = bool,501		QueryKind = ValueQuery,502	>;503504	/// Not used by code, exists only to provide some types to metadata505	#[pallet::storage]506	pub type DummyStorageValue<T: Config> = StorageValue<507		Value = (508			CollectionStats,509			CollectionId,510			TokenId,511			TokenChild,512			PhantomType<(513				TokenData<T::CrossAccountId>,514				RpcCollection<T::AccountId>,515				// RMRK516				RmrkCollectionInfo<T::AccountId>,517				RmrkInstanceInfo<T::AccountId>,518				RmrkResourceInfo,519				RmrkPropertyInfo,520				RmrkBaseInfo<T::AccountId>,521				RmrkPartType,522				RmrkTheme,523				RmrkNftChild,524			)>,525		),526		QueryKind = OptionQuery,527	>;528529	#[pallet::hooks]530	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {531		fn on_runtime_upgrade() -> Weight {532			if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {533				use up_data_structs::{CollectionVersion1, CollectionVersion2};534				<CollectionById<T>>::translate::<CollectionVersion1<T::AccountId>, _>(|id, v| {535					let mut props = Vec::new();536					if !v.offchain_schema.is_empty() {537						props.push(Property {538							key: b"_old_offchainSchema".to_vec().try_into().unwrap(),539							value: v540								.offchain_schema541								.clone()542								.into_inner()543								.try_into()544								.expect("offchain schema too big"),545						});546					}547					if !v.variable_on_chain_schema.is_empty() {548						props.push(Property {549							key: b"_old_variableOnChainSchema".to_vec().try_into().unwrap(),550							value: v551								.variable_on_chain_schema552								.clone()553								.into_inner()554								.try_into()555								.expect("offchain schema too big"),556						});557					}558					if !v.const_on_chain_schema.is_empty() {559						props.push(Property {560							key: b"_old_constOnChainSchema".to_vec().try_into().unwrap(),561							value: v562								.const_on_chain_schema563								.clone()564								.into_inner()565								.try_into()566								.expect("offchain schema too big"),567						});568					}569					props.push(Property {570						key: b"_old_schemaVersion".to_vec().try_into().unwrap(),571						value: match v.schema_version {572							SchemaVersion::ImageURL => b"ImageUrl".as_slice(),573							SchemaVersion::Unique => b"Unique".as_slice(),574						}575						.to_vec()576						.try_into()577						.unwrap(),578					});579					Self::set_scoped_collection_properties(580						id,581						PropertyScope::None,582						props.into_iter(),583					)584					.expect("existing data larger than properties");585					let mut new = CollectionVersion2::from(v.clone());586					new.permissions.access = Some(v.access);587					new.permissions.mint_mode = Some(v.mint_mode);588					Some(new)589				});590			}591592			0593		}594	}595}596597impl<T: Config> Pallet<T> {598	/// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens599	pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {600		ensure!(601			&T::CrossAccountId::from_eth(H160([0; 20])) != receiver,602			<Error<T>>::AddressIsZero603		);604		Ok(())605	}606	pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {607		<IsAdmin<T>>::iter_prefix((collection,))608			.map(|(a, _)| a)609			.collect()610	}611	pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {612		<Allowlist<T>>::iter_prefix((collection,))613			.map(|(a, _)| a)614			.collect()615	}616	pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {617		<Allowlist<T>>::get((collection, user))618	}619	pub fn collection_stats() -> CollectionStats {620		let created = <CreatedCollectionCount<T>>::get();621		let destroyed = <DestroyedCollectionCount<T>>::get();622		CollectionStats {623			created: created.0,624			destroyed: destroyed.0,625			alive: created.0 - destroyed.0,626		}627	}628629	pub fn effective_collection_limits(collection: CollectionId) -> Option<CollectionLimits> {630		let collection = <CollectionById<T>>::get(collection);631		if collection.is_none() {632			return None;633		}634635		let collection = collection.unwrap();636		let limits = collection.limits;637		let effective_limits = CollectionLimits {638			account_token_ownership_limit: Some(limits.account_token_ownership_limit()),639			sponsored_data_size: Some(limits.sponsored_data_size()),640			sponsored_data_rate_limit: Some(641				limits642					.sponsored_data_rate_limit643					.unwrap_or(SponsoringRateLimit::SponsoringDisabled),644			),645			token_limit: Some(limits.token_limit()),646			sponsor_transfer_timeout: Some(limits.sponsor_transfer_timeout(647				match collection.mode {648					CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,649					CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,650					CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,651				},652			)),653			sponsor_approve_timeout: Some(limits.sponsor_approve_timeout()),654			owner_can_transfer: Some(limits.owner_can_transfer()),655			owner_can_destroy: Some(limits.owner_can_destroy()),656			transfers_enabled: Some(limits.transfers_enabled()),657		};658659		Some(effective_limits)660	}661662	pub fn rpc_collection(collection: CollectionId) -> Option<RpcCollection<T::AccountId>> {663		let Collection {664			name,665			description,666			owner,667			mode,668			token_prefix,669			sponsorship,670			limits,671			permissions,672		} = <CollectionById<T>>::get(collection)?;673674		let token_property_permissions = <CollectionPropertyPermissions<T>>::get(collection)675			.into_iter()676			.map(|(key, permission)| PropertyKeyPermission { key, permission })677			.collect();678679		let properties = <CollectionProperties<T>>::get(collection)680			.into_iter()681			.map(|(key, value)| Property { key, value })682			.collect();683684		let permissions = CollectionPermissions {685			access: Some(permissions.access()),686			mint_mode: Some(permissions.mint_mode()),687			nesting: Some(permissions.nesting().clone()),688		};689690		Some(RpcCollection {691			name: name.into_inner(),692			description: description.into_inner(),693			owner,694			mode,695			token_prefix: token_prefix.into_inner(),696			sponsorship,697			limits,698			permissions,699			token_property_permissions,700			properties,701		})702	}703}704705macro_rules! limit_default {706	($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{707		$(708			if let Some($new) = $new.$field {709				let $old = $old.$field($($arg)?);710				let _ = $new;711				let _ = $old;712				$check713			} else {714				$new.$field = $old.$field715			}716		)*717	}};718}719macro_rules! limit_default_clone {720	($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{721		$(722			if let Some($new) = $new.$field.clone() {723				let $old = $old.$field($($arg)?);724				let _ = $new;725				let _ = $old;726				$check727			} else {728				$new.$field = $old.$field.clone()729			}730		)*731	}};732}733734impl<T: Config> Pallet<T> {735	pub fn init_collection(736		owner: T::CrossAccountId,737		data: CreateCollectionData<T::AccountId>,738	) -> Result<CollectionId, DispatchError> {739		{740			ensure!(741				data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH as usize,742				Error::<T>::CollectionTokenPrefixLimitExceeded743			);744		}745746		let created_count = <CreatedCollectionCount<T>>::get()747			.0748			.checked_add(1)749			.ok_or(ArithmeticError::Overflow)?;750		let destroyed_count = <DestroyedCollectionCount<T>>::get().0;751		let id = CollectionId(created_count);752753		// bound Total number of collections754		ensure!(755			created_count - destroyed_count <= COLLECTION_NUMBER_LIMIT,756			<Error<T>>::TotalCollectionsLimitExceeded757		);758759		// =========760761		let collection = Collection {762			owner: owner.as_sub().clone(),763			name: data.name,764			mode: data.mode.clone(),765			description: data.description,766			token_prefix: data.token_prefix,767			sponsorship: data768				.pending_sponsor769				.map(SponsorshipState::Unconfirmed)770				.unwrap_or_default(),771			limits: data772				.limits773				.map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits))774				.unwrap_or_else(|| Ok(CollectionLimits::default()))?,775			permissions: data776				.permissions777				.map(|permissions| {778					Self::clamp_permissions(data.mode.clone(), &Default::default(), permissions)779				})780				.unwrap_or_else(|| Ok(CollectionPermissions::default()))?,781		};782783		let mut collection_properties = up_data_structs::CollectionProperties::get();784		collection_properties785			.try_set_from_iter(data.properties.into_iter())786			.map_err(<Error<T>>::from)?;787788		CollectionProperties::<T>::insert(id, collection_properties);789790		let mut token_props_permissions = PropertiesPermissionMap::new();791		token_props_permissions792			.try_set_from_iter(data.token_property_permissions.into_iter())793			.map_err(<Error<T>>::from)?;794795		CollectionPropertyPermissions::<T>::insert(id, token_props_permissions);796797		// Take a (non-refundable) deposit of collection creation798		{799			let mut imbalance =800				<<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();801			imbalance.subsume(802				<<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(803					&T::TreasuryAccountId::get(),804					T::CollectionCreationPrice::get(),805				),806			);807			<T as Config>::Currency::settle(808				&owner.as_sub(),809				imbalance,810				WithdrawReasons::TRANSFER,811				ExistenceRequirement::KeepAlive,812			)813			.map_err(|_| Error::<T>::NotSufficientFounds)?;814		}815816		<CreatedCollectionCount<T>>::put(created_count);817		<Pallet<T>>::deposit_event(Event::CollectionCreated(818			id,819			data.mode.id(),820			owner.as_sub().clone(),821		));822		<PalletEvm<T>>::deposit_log(823			erc::CollectionHelpersEvents::CollectionCreated {824				owner: *owner.as_eth(),825				collection_id: eth::collection_id_to_address(id),826			}827			.to_log(T::ContractAddress::get()),828		);829		<CollectionById<T>>::insert(id, collection);830		Ok(id)831	}832833	pub fn destroy_collection(834		collection: CollectionHandle<T>,835		sender: &T::CrossAccountId,836	) -> DispatchResult {837		ensure!(838			collection.limits.owner_can_destroy(),839			<Error<T>>::NoPermission,840		);841		collection.check_is_owner(sender)?;842843		let destroyed_collections = <DestroyedCollectionCount<T>>::get()844			.0845			.checked_add(1)846			.ok_or(ArithmeticError::Overflow)?;847848		// =========849850		<DestroyedCollectionCount<T>>::put(destroyed_collections);851		<CollectionById<T>>::remove(collection.id);852		<AdminAmount<T>>::remove(collection.id);853		<IsAdmin<T>>::remove_prefix((collection.id,), None);854		<Allowlist<T>>::remove_prefix((collection.id,), None);855		<CollectionProperties<T>>::remove(collection.id);856857		<Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));858		Ok(())859	}860861	pub fn set_collection_property(862		collection: &CollectionHandle<T>,863		sender: &T::CrossAccountId,864		property: Property,865	) -> DispatchResult {866		collection.check_is_owner_or_admin(sender)?;867868		CollectionProperties::<T>::try_mutate(collection.id, |properties| {869			let property = property.clone();870			properties.try_set(property.key, property.value)871		})872		.map_err(<Error<T>>::from)?;873874		Self::deposit_event(Event::CollectionPropertySet(collection.id, property.key));875876		Ok(())877	}878879	pub fn set_scoped_collection_property(880		collection_id: CollectionId,881		scope: PropertyScope,882		property: Property,883	) -> DispatchResult {884		CollectionProperties::<T>::try_mutate(collection_id, |properties| {885			properties.try_scoped_set(scope, property.key, property.value)886		})887		.map_err(<Error<T>>::from)?;888889		Ok(())890	}891892	pub fn set_scoped_collection_properties(893		collection_id: CollectionId,894		scope: PropertyScope,895		properties: impl Iterator<Item = Property>,896	) -> DispatchResult {897		CollectionProperties::<T>::try_mutate(collection_id, |stored_properties| {898			stored_properties.try_scoped_set_from_iter(scope, properties)899		})900		.map_err(<Error<T>>::from)?;901902		Ok(())903	}904905	#[transactional]906	pub fn set_collection_properties(907		collection: &CollectionHandle<T>,908		sender: &T::CrossAccountId,909		properties: Vec<Property>,910	) -> DispatchResult {911		for property in properties {912			Self::set_collection_property(collection, sender, property)?;913		}914915		Ok(())916	}917918	pub fn delete_collection_property(919		collection: &CollectionHandle<T>,920		sender: &T::CrossAccountId,921		property_key: PropertyKey,922	) -> DispatchResult {923		collection.check_is_owner_or_admin(sender)?;924925		CollectionProperties::<T>::try_mutate(collection.id, |properties| {926			properties.remove(&property_key)927		})928		.map_err(<Error<T>>::from)?;929930		Self::deposit_event(Event::CollectionPropertyDeleted(931			collection.id,932			property_key,933		));934935		Ok(())936	}937938	#[transactional]939	pub fn delete_collection_properties(940		collection: &CollectionHandle<T>,941		sender: &T::CrossAccountId,942		property_keys: Vec<PropertyKey>,943	) -> DispatchResult {944		for key in property_keys {945			Self::delete_collection_property(collection, sender, key)?;946		}947948		Ok(())949	}950951	// For migrations952	pub fn set_property_permission_unchecked(953		collection: CollectionId,954		property_permission: PropertyKeyPermission,955	) -> DispatchResult {956		<CollectionPropertyPermissions<T>>::try_mutate(collection, |permissions| {957			permissions.try_set(property_permission.key, property_permission.permission)958		})959		.map_err(<Error<T>>::from)?;960		Ok(())961	}962963	pub fn set_property_permission(964		collection: &CollectionHandle<T>,965		sender: &T::CrossAccountId,966		property_permission: PropertyKeyPermission,967	) -> DispatchResult {968		collection.check_is_owner_or_admin(sender)?;969970		let all_permissions = CollectionPropertyPermissions::<T>::get(collection.id);971		let current_permission = all_permissions.get(&property_permission.key);972		if matches![973			current_permission,974			Some(PropertyPermission { mutable: false, .. })975		] {976			return Err(<Error<T>>::NoPermission.into());977		}978979		CollectionPropertyPermissions::<T>::try_mutate(collection.id, |permissions| {980			let property_permission = property_permission.clone();981			permissions.try_set(property_permission.key, property_permission.permission)982		})983		.map_err(<Error<T>>::from)?;984985		Self::deposit_event(Event::PropertyPermissionSet(986			collection.id,987			property_permission.key,988		));989990		Ok(())991	}992993	#[transactional]994	pub fn set_property_permissions(995		collection: &CollectionHandle<T>,996		sender: &T::CrossAccountId,997		property_permissions: Vec<PropertyKeyPermission>,998	) -> DispatchResult {999		for prop_pemission in property_permissions {1000			Self::set_property_permission(collection, sender, prop_pemission)?;1001		}10021003		Ok(())1004	}10051006	pub fn get_collection_property(1007		collection_id: CollectionId,1008		key: &PropertyKey,1009	) -> Option<PropertyValue> {1010		Self::collection_properties(collection_id).get(key).cloned()1011	}10121013	pub fn bytes_keys_to_property_keys(1014		keys: Vec<Vec<u8>>,1015	) -> Result<Vec<PropertyKey>, DispatchError> {1016		keys.into_iter()1017			.map(|key| -> Result<PropertyKey, DispatchError> {1018				key.try_into()1019					.map_err(|_| <Error<T>>::PropertyKeyIsTooLong.into())1020			})1021			.collect::<Result<Vec<PropertyKey>, DispatchError>>()1022	}10231024	pub fn filter_collection_properties(1025		collection_id: CollectionId,1026		keys: Option<Vec<PropertyKey>>,1027	) -> Result<Vec<Property>, DispatchError> {1028		let properties = Self::collection_properties(collection_id);10291030		let properties = keys1031			.map(|keys| {1032				keys.into_iter()1033					.filter_map(|key| {1034						properties.get(&key).map(|value| Property {1035							key,1036							value: value.clone(),1037						})1038					})1039					.collect()1040			})1041			.unwrap_or_else(|| {1042				properties1043					.into_iter()1044					.map(|(key, value)| Property { key, value })1045					.collect()1046			});10471048		Ok(properties)1049	}10501051	pub fn filter_property_permissions(1052		collection_id: CollectionId,1053		keys: Option<Vec<PropertyKey>>,1054	) -> Result<Vec<PropertyKeyPermission>, DispatchError> {1055		let permissions = Self::property_permissions(collection_id);10561057		let key_permissions = keys1058			.map(|keys| {1059				keys.into_iter()1060					.filter_map(|key| {1061						permissions1062							.get(&key)1063							.map(|permission| PropertyKeyPermission {1064								key,1065								permission: permission.clone(),1066							})1067					})1068					.collect()1069			})1070			.unwrap_or_else(|| {1071				permissions1072					.into_iter()1073					.map(|(key, permission)| PropertyKeyPermission { key, permission })1074					.collect()1075			});10761077		Ok(key_permissions)1078	}10791080	pub fn toggle_allowlist(1081		collection: &CollectionHandle<T>,1082		sender: &T::CrossAccountId,1083		user: &T::CrossAccountId,1084		allowed: bool,1085	) -> DispatchResult {1086		collection.check_is_owner_or_admin(sender)?;10871088		// =========10891090		if allowed {1091			<Allowlist<T>>::insert((collection.id, user), true);1092		} else {1093			<Allowlist<T>>::remove((collection.id, user));1094		}10951096		Ok(())1097	}10981099	pub fn toggle_admin(1100		collection: &CollectionHandle<T>,1101		sender: &T::CrossAccountId,1102		user: &T::CrossAccountId,1103		admin: bool,1104	) -> DispatchResult {1105		collection.check_is_owner_or_admin(sender)?;11061107		let was_admin = <IsAdmin<T>>::get((collection.id, user));1108		if was_admin == admin {1109			return Ok(());1110		}1111		let amount = <AdminAmount<T>>::get(collection.id);11121113		if admin {1114			let amount = amount1115				.checked_add(1)1116				.ok_or(<Error<T>>::CollectionAdminCountExceeded)?;1117			ensure!(1118				amount <= Self::collection_admins_limit(),1119				<Error<T>>::CollectionAdminCountExceeded,1120			);11211122			// =========11231124			<AdminAmount<T>>::insert(collection.id, amount);1125			<IsAdmin<T>>::insert((collection.id, user), true);1126		} else {1127			<AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));1128			<IsAdmin<T>>::remove((collection.id, user));1129		}11301131		Ok(())1132	}11331134	pub fn clamp_limits(1135		mode: CollectionMode,1136		old_limit: &CollectionLimits,1137		mut new_limit: CollectionLimits,1138	) -> Result<CollectionLimits, DispatchError> {1139		limit_default!(old_limit, new_limit,1140			account_token_ownership_limit => ensure!(1141				new_limit <= MAX_TOKEN_OWNERSHIP,1142				<Error<T>>::CollectionLimitBoundsExceeded,1143			),1144			sponsored_data_size => ensure!(1145				new_limit <= CUSTOM_DATA_LIMIT,1146				<Error<T>>::CollectionLimitBoundsExceeded,1147			),11481149			sponsored_data_rate_limit => {},1150			token_limit => ensure!(1151				old_limit >= new_limit && new_limit > 0,1152				<Error<T>>::CollectionTokenLimitExceeded1153			),11541155			sponsor_transfer_timeout(match mode {1156				CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,1157				CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,1158				CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,1159			}) => ensure!(1160				new_limit <= MAX_SPONSOR_TIMEOUT,1161				<Error<T>>::CollectionLimitBoundsExceeded,1162			),1163			sponsor_approve_timeout => {},1164			owner_can_transfer => ensure!(1165				old_limit || !new_limit,1166				<Error<T>>::OwnerPermissionsCantBeReverted,1167			),1168			owner_can_destroy => ensure!(1169				old_limit || !new_limit,1170				<Error<T>>::OwnerPermissionsCantBeReverted,1171			),1172			transfers_enabled => {},1173		);1174		Ok(new_limit)1175	}11761177	pub fn clamp_permissions(1178		_mode: CollectionMode,1179		old_limit: &CollectionPermissions,1180		mut new_limit: CollectionPermissions,1181	) -> Result<CollectionPermissions, DispatchError> {1182		limit_default_clone!(old_limit, new_limit,1183			access => {},1184			mint_mode => {},1185			nesting => {},1186		);1187		Ok(new_limit)1188	}1189}11901191#[macro_export]1192macro_rules! unsupported {1193	() => {1194		Err(<Error<T>>::UnsupportedOperation.into())1195	};1196}11971198/// Worst cases1199pub trait CommonWeightInfo<CrossAccountId> {1200	fn create_item() -> Weight;1201	fn create_multiple_items(amount: &[CreateItemData]) -> Weight;1202	fn create_multiple_items_ex(cost: &CreateItemExData<CrossAccountId>) -> Weight;1203	fn burn_item() -> Weight;1204	fn set_collection_properties(amount: u32) -> Weight;1205	fn delete_collection_properties(amount: u32) -> Weight;1206	fn set_token_properties(amount: u32) -> Weight;1207	fn delete_token_properties(amount: u32) -> Weight;1208	fn set_property_permissions(amount: u32) -> Weight;1209	fn transfer() -> Weight;1210	fn approve() -> Weight;1211	fn transfer_from() -> Weight;1212	fn burn_from() -> Weight;12131214	/// Differs from burn_item in case of Fungible and Refungible, as it should burn1215	/// whole users's balance1216	///1217	/// This method shouldn't be used directly, as it doesn't count breadth price, use `burn_recursively` instead1218	fn burn_recursively_self_raw() -> Weight;1219	/// Cost of iterating over `amount` children while burning, without counting child burning itself1220	///1221	/// This method shouldn't be used directly, as it doesn't count depth price, use `burn_recursively` instead1222	fn burn_recursively_breadth_raw(amount: u32) -> Weight;12231224	fn burn_recursively(max_selfs: u32, max_breadth: u32) -> Weight {1225		Self::burn_recursively_self_raw()1226			.saturating_mul(max_selfs.max(1) as u64)1227			.saturating_add(Self::burn_recursively_breadth_raw(max_breadth))1228	}1229}12301231pub trait CommonCollectionOperations<T: Config> {1232	fn create_item(1233		&self,1234		sender: T::CrossAccountId,1235		to: T::CrossAccountId,1236		data: CreateItemData,1237		nesting_budget: &dyn Budget,1238	) -> DispatchResultWithPostInfo;1239	fn create_multiple_items(1240		&self,1241		sender: T::CrossAccountId,1242		to: T::CrossAccountId,1243		data: Vec<CreateItemData>,1244		nesting_budget: &dyn Budget,1245	) -> DispatchResultWithPostInfo;1246	fn create_multiple_items_ex(1247		&self,1248		sender: T::CrossAccountId,1249		data: CreateItemExData<T::CrossAccountId>,1250		nesting_budget: &dyn Budget,1251	) -> DispatchResultWithPostInfo;1252	fn burn_item(1253		&self,1254		sender: T::CrossAccountId,1255		token: TokenId,1256		amount: u128,1257	) -> DispatchResultWithPostInfo;1258	fn burn_item_recursively(1259		&self,1260		sender: T::CrossAccountId,1261		token: TokenId,1262		self_budget: &dyn Budget,1263		breadth_budget: &dyn Budget,1264	) -> DispatchResultWithPostInfo;1265	fn set_collection_properties(1266		&self,1267		sender: T::CrossAccountId,1268		properties: Vec<Property>,1269	) -> DispatchResultWithPostInfo;1270	fn delete_collection_properties(1271		&self,1272		sender: &T::CrossAccountId,1273		property_keys: Vec<PropertyKey>,1274	) -> DispatchResultWithPostInfo;1275	fn set_token_properties(1276		&self,1277		sender: T::CrossAccountId,1278		token_id: TokenId,1279		property: Vec<Property>,1280	) -> DispatchResultWithPostInfo;1281	fn delete_token_properties(1282		&self,1283		sender: T::CrossAccountId,1284		token_id: TokenId,1285		property_keys: Vec<PropertyKey>,1286	) -> DispatchResultWithPostInfo;1287	fn set_property_permissions(1288		&self,1289		sender: &T::CrossAccountId,1290		property_permissions: Vec<PropertyKeyPermission>,1291	) -> DispatchResultWithPostInfo;1292	fn transfer(1293		&self,1294		sender: T::CrossAccountId,1295		to: T::CrossAccountId,1296		token: TokenId,1297		amount: u128,1298		nesting_budget: &dyn Budget,1299	) -> DispatchResultWithPostInfo;1300	fn approve(1301		&self,1302		sender: T::CrossAccountId,1303		spender: T::CrossAccountId,1304		token: TokenId,1305		amount: u128,1306	) -> DispatchResultWithPostInfo;1307	fn transfer_from(1308		&self,1309		sender: T::CrossAccountId,1310		from: T::CrossAccountId,1311		to: T::CrossAccountId,1312		token: TokenId,1313		amount: u128,1314		nesting_budget: &dyn Budget,1315	) -> DispatchResultWithPostInfo;1316	fn burn_from(1317		&self,1318		sender: T::CrossAccountId,1319		from: T::CrossAccountId,1320		token: TokenId,1321		amount: u128,1322		nesting_budget: &dyn Budget,1323	) -> DispatchResultWithPostInfo;13241325	fn check_nesting(1326		&self,1327		sender: T::CrossAccountId,1328		from: (CollectionId, TokenId),1329		under: TokenId,1330		budget: &dyn Budget,1331	) -> DispatchResult;13321333	fn nest(&self, under: TokenId, to_nest: (CollectionId, TokenId));13341335	fn unnest(&self, under: TokenId, to_nest: (CollectionId, TokenId));13361337	fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;1338	fn collection_tokens(&self) -> Vec<TokenId>;1339	fn token_exists(&self, token: TokenId) -> bool;1340	fn last_token_id(&self) -> TokenId;13411342	fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId>;1343	fn token_property(&self, token_id: TokenId, key: &PropertyKey) -> Option<PropertyValue>;1344	fn token_properties(&self, token_id: TokenId, keys: Option<Vec<PropertyKey>>) -> Vec<Property>;1345	/// Amount of unique collection tokens1346	fn total_supply(&self) -> u32;1347	/// Amount of different tokens account has (Applicable to nonfungible/refungible)1348	fn account_balance(&self, account: T::CrossAccountId) -> u32;1349	/// Amount of specific token account have (Applicable to fungible/refungible)1350	fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;1351	fn allowance(1352		&self,1353		sender: T::CrossAccountId,1354		spender: T::CrossAccountId,1355		token: TokenId,1356	) -> u128;1357}13581359// Flexible enough for implementing CommonCollectionOperations1360pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {1361	let post_info = PostDispatchInfo {1362		actual_weight: Some(weight),1363		pays_fee: Pays::Yes,1364	};1365	match res {1366		Ok(()) => Ok(post_info),1367		Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),1368	}1369}13701371impl<T: Config> From<PropertiesError> for Error<T> {1372	fn from(error: PropertiesError) -> Self {1373		match error {1374			PropertiesError::NoSpaceForProperty => Self::NoSpaceForProperty,1375			PropertiesError::PropertyLimitReached => Self::PropertyLimitReached,1376			PropertiesError::InvalidCharacterInPropertyKey => Self::InvalidCharacterInPropertyKey,1377			PropertiesError::PropertyKeyIsTooLong => Self::PropertyKeyIsTooLong,1378			PropertiesError::EmptyPropertyKey => Self::EmptyPropertyKey,1379		}1380	}1381}
after · pallets/common/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)]1819extern crate alloc;2021use core::ops::{Deref, DerefMut};22use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};23use sp_std::vec::Vec;24use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};25use evm_coder::ToLog;26use frame_support::{27	dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, Weight, PostDispatchInfo},28	ensure,29	traits::{Imbalance, Get, Currency, WithdrawReasons, ExistenceRequirement},30	weights::Pays,31	transactional,32};33use pallet_evm::GasWeightMapping;34use up_data_structs::{35	COLLECTION_NUMBER_LIMIT,36	Collection,37	RpcCollection,38	CollectionId,39	CreateItemData,40	MAX_TOKEN_PREFIX_LENGTH,41	COLLECTION_ADMINS_LIMIT,42	TokenId,43	TokenChild,44	CollectionStats,45	MAX_TOKEN_OWNERSHIP,46	CollectionMode,47	NFT_SPONSOR_TRANSFER_TIMEOUT,48	FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,49	REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,50	MAX_SPONSOR_TIMEOUT,51	CUSTOM_DATA_LIMIT,52	CollectionLimits,53	CreateCollectionData,54	SponsorshipState,55	CreateItemExData,56	SponsoringRateLimit,57	budget::Budget,58	PhantomType,59	Property,60	Properties,61	PropertiesPermissionMap,62	PropertyKey,63	PropertyValue,64	PropertyPermission,65	PropertiesError,66	PropertyKeyPermission,67	TokenData,68	TrySetProperty,69	PropertyScope,70	// RMRK71	RmrkCollectionInfo,72	RmrkInstanceInfo,73	RmrkResourceInfo,74	RmrkPropertyInfo,75	RmrkBaseInfo,76	RmrkPartType,77	RmrkTheme,78	RmrkNftChild,79	CollectionPermissions,80	SchemaVersion,81};8283pub use pallet::*;84use sp_core::H160;85use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};86#[cfg(feature = "runtime-benchmarks")]87pub mod benchmarking;88pub mod dispatch;89pub mod erc;90pub mod eth;91pub mod weights;9293pub type SelfWeightOf<T> = <T as Config>::WeightInfo;9495#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]96pub struct CollectionHandle<T: Config> {97	pub id: CollectionId,98	collection: Collection<T::AccountId>,99	pub recorder: SubstrateRecorder<T>,100}101impl<T: Config> WithRecorder<T> for CollectionHandle<T> {102	fn recorder(&self) -> &SubstrateRecorder<T> {103		&self.recorder104	}105	fn into_recorder(self) -> SubstrateRecorder<T> {106		self.recorder107	}108}109impl<T: Config> CollectionHandle<T> {110	pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {111		<CollectionById<T>>::get(id).map(|collection| Self {112			id,113			collection,114			recorder: SubstrateRecorder::new(gas_limit),115		})116	}117118	pub fn new_with_recorder(id: CollectionId, recorder: SubstrateRecorder<T>) -> Option<Self> {119		<CollectionById<T>>::get(id).map(|collection| Self {120			id,121			collection,122			recorder,123		})124	}125126	pub fn new(id: CollectionId) -> Option<Self> {127		Self::new_with_gas_limit(id, u64::MAX)128	}129130	pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {131		Ok(Self::new(id).ok_or(<Error<T>>::CollectionNotFound)?)132	}133134	pub fn consume_store_reads(&self, reads: u64) -> evm_coder::execution::Result<()> {135		self.recorder136			.consume_gas(T::GasWeightMapping::weight_to_gas(137				<T as frame_system::Config>::DbWeight::get()138					.read139					.saturating_mul(reads),140			))141	}142143	pub fn consume_store_writes(&self, writes: u64) -> evm_coder::execution::Result<()> {144		self.recorder145			.consume_gas(T::GasWeightMapping::weight_to_gas(146				<T as frame_system::Config>::DbWeight::get()147					.write148					.saturating_mul(writes),149			))150	}151	pub fn save(self) -> Result<(), DispatchError> {152		self.check_is_read_only()?;153		<CollectionById<T>>::insert(self.id, self.collection);154		Ok(())155	}156157	pub fn set_sponsor(&mut self, sponsor: T::AccountId) -> DispatchResult {158		self.check_is_read_only()?;159		self.collection.sponsorship = SponsorshipState::Unconfirmed(sponsor);160		Ok(())161	}162163	pub fn confirm_sponsorship(&mut self, sender: &T::AccountId) -> Result<bool, DispatchError> {164		self.check_is_read_only()?;165166		if self.collection.sponsorship.pending_sponsor() != Some(sender) {167			return Ok(false);168		}169170		self.collection.sponsorship = SponsorshipState::Confirmed(sender.clone());171		Ok(true)172	}173174	pub fn check_is_read_only(&self) -> DispatchResult {175		if self.read_only {176			return Err(<Error<T>>::CollectionNotFound)?;177		}178		179		Ok(())180	}181}182183impl<T: Config> Deref for CollectionHandle<T> {184	type Target = Collection<T::AccountId>;185186	fn deref(&self) -> &Self::Target {187		&self.collection188	}189}190191impl<T: Config> DerefMut for CollectionHandle<T> {192	fn deref_mut(&mut self) -> &mut Self::Target {193		&mut self.collection194	}195}196197impl<T: Config> CollectionHandle<T> {198	pub fn check_is_owner(&self, subject: &T::CrossAccountId) -> DispatchResult {199		ensure!(*subject.as_sub() == self.owner, <Error<T>>::NoPermission);200		Ok(())201	}202	pub fn is_owner_or_admin(&self, subject: &T::CrossAccountId) -> bool {203		*subject.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, subject))204	}205	pub fn check_is_owner_or_admin(&self, subject: &T::CrossAccountId) -> DispatchResult {206		ensure!(self.is_owner_or_admin(subject), <Error<T>>::NoPermission);207		Ok(())208	}209	pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool {210		self.limits.owner_can_transfer() && self.is_owner_or_admin(user)211	}212	pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool {213		self.limits.owner_can_transfer() && self.is_owner_or_admin(user)214	}215	pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {216		ensure!(217			<Allowlist<T>>::get((self.id, user)),218			<Error<T>>::AddressNotInAllowlist219		);220		Ok(())221	}222}223224#[frame_support::pallet]225pub mod pallet {226	use super::*;227	use pallet_evm::account;228	use dispatch::CollectionDispatch;229	use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key, traits::StorageVersion};230	use frame_system::pallet_prelude::*;231	use frame_support::traits::Currency;232	use up_data_structs::{TokenId, mapping::TokenAddressMapping};233	use scale_info::TypeInfo;234	use weights::WeightInfo;235236	#[pallet::config]237	pub trait Config:238		frame_system::Config239		+ pallet_evm_coder_substrate::Config240		+ pallet_evm::Config241		+ TypeInfo242		+ account::Config243	{244		type WeightInfo: WeightInfo;245		type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;246247		type Currency: Currency<Self::AccountId>;248249		#[pallet::constant]250		type CollectionCreationPrice: Get<251			<<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,252		>;253		type CollectionDispatch: CollectionDispatch<Self>;254255		type TreasuryAccountId: Get<Self::AccountId>;256		type ContractAddress: Get<H160>;257258		type EvmTokenAddressMapping: TokenAddressMapping<H160>;259		type CrossTokenAddressMapping: TokenAddressMapping<Self::CrossAccountId>;260	}261262	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);263264	#[pallet::pallet]265	#[pallet::storage_version(STORAGE_VERSION)]266	#[pallet::generate_store(pub(super) trait Store)]267	pub struct Pallet<T>(_);268269	#[pallet::extra_constants]270	impl<T: Config> Pallet<T> {271		pub fn collection_admins_limit() -> u32 {272			COLLECTION_ADMINS_LIMIT273		}274	}275276	#[pallet::event]277	#[pallet::generate_deposit(pub fn deposit_event)]278	pub enum Event<T: Config> {279		/// New collection was created280		///281		/// # Arguments282		///283		/// * collection_id: Globally unique identifier of newly created collection.284		///285		/// * mode: [CollectionMode] converted into u8.286		///287		/// * account_id: Collection owner.288		CollectionCreated(CollectionId, u8, T::AccountId),289290		/// New collection was destroyed291		///292		/// # Arguments293		///294		/// * collection_id: Globally unique identifier of collection.295		CollectionDestroyed(CollectionId),296297		/// New item was created.298		///299		/// # Arguments300		///301		/// * collection_id: Id of the collection where item was created.302		///303		/// * item_id: Id of an item. Unique within the collection.304		///305		/// * recipient: Owner of newly created item306		///307		/// * amount: Always 1 for NFT308		ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128),309310		/// Collection item was burned.311		///312		/// # Arguments313		///314		/// * collection_id.315		///316		/// * item_id: Identifier of burned NFT.317		///318		/// * owner: which user has destroyed its tokens319		///320		/// * amount: Always 1 for NFT321		ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128),322323		/// Item was transferred324		///325		/// * collection_id: Id of collection to which item is belong326		///327		/// * item_id: Id of an item328		///329		/// * sender: Original owner of item330		///331		/// * recipient: New owner of item332		///333		/// * amount: Always 1 for NFT334		Transfer(335			CollectionId,336			TokenId,337			T::CrossAccountId,338			T::CrossAccountId,339			u128,340		),341342		/// * collection_id343		///344		/// * item_id345		///346		/// * sender347		///348		/// * spender349		///350		/// * amount351		Approved(352			CollectionId,353			TokenId,354			T::CrossAccountId,355			T::CrossAccountId,356			u128,357		),358359		CollectionPropertySet(CollectionId, PropertyKey),360361		CollectionPropertyDeleted(CollectionId, PropertyKey),362363		TokenPropertySet(CollectionId, TokenId, PropertyKey),364365		TokenPropertyDeleted(CollectionId, TokenId, PropertyKey),366367		PropertyPermissionSet(CollectionId, PropertyKey),368	}369370	#[pallet::error]371	pub enum Error<T> {372		/// This collection does not exist.373		CollectionNotFound,374		/// Sender parameter and item owner must be equal.375		MustBeTokenOwner,376		/// No permission to perform action377		NoPermission,378		/// Destroying only empty collections is allowed379		CantDestroyNotEmptyCollection,380		/// Collection is not in mint mode.381		PublicMintingNotAllowed,382		/// Address is not in allow list.383		AddressNotInAllowlist,384385		/// Collection name can not be longer than 63 char.386		CollectionNameLimitExceeded,387		/// Collection description can not be longer than 255 char.388		CollectionDescriptionLimitExceeded,389		/// Token prefix can not be longer than 15 char.390		CollectionTokenPrefixLimitExceeded,391		/// Total collections bound exceeded.392		TotalCollectionsLimitExceeded,393		/// Exceeded max admin count394		CollectionAdminCountExceeded,395		/// Collection limit bounds per collection exceeded396		CollectionLimitBoundsExceeded,397		/// Tried to enable permissions which are only permitted to be disabled398		OwnerPermissionsCantBeReverted,399		/// Collection settings not allowing items transferring400		TransferNotAllowed,401		/// Account token limit exceeded per collection402		AccountTokenLimitExceeded,403		/// Collection token limit exceeded404		CollectionTokenLimitExceeded,405		/// Metadata flag frozen406		MetadataFlagFrozen,407408		/// Item not exists.409		TokenNotFound,410		/// Item balance not enough.411		TokenValueTooLow,412		/// Requested value more than approved.413		ApprovedValueTooLow,414		/// Tried to approve more than owned415		CantApproveMoreThanOwned,416417		/// Can't transfer tokens to ethereum zero address418		AddressIsZero,419		/// Target collection doesn't supports this operation420		UnsupportedOperation,421422		/// Not sufficient founds to perform action423		NotSufficientFounds,424425		/// Collection has nesting disabled426		NestingIsDisabled,427		/// Only owner may nest tokens under this collection428		OnlyOwnerAllowedToNest,429		/// Only tokens from specific collections may nest tokens under this430		SourceCollectionIsNotAllowedToNest,431432		/// Tried to store more data than allowed in collection field433		CollectionFieldSizeExceeded,434435		/// Tried to store more property data than allowed436		NoSpaceForProperty,437438		/// Tried to store more property keys than allowed439		PropertyLimitReached,440441		/// Property key is too long442		PropertyKeyIsTooLong,443444		/// Only ASCII letters, digits, and '_', '-' are allowed445		InvalidCharacterInPropertyKey,446447		/// Empty property keys are forbidden448		EmptyPropertyKey,449450		/// Collection is read only451		CollectionIsReadOnly,452	}453454	#[pallet::storage]455	pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;456	#[pallet::storage]457	pub type DestroyedCollectionCount<T> =458		StorageValue<Value = CollectionId, QueryKind = ValueQuery>;459460	/// Collection info461	#[pallet::storage]462	pub type CollectionById<T> = StorageMap<463		Hasher = Blake2_128Concat,464		Key = CollectionId,465		Value = Collection<<T as frame_system::Config>::AccountId>,466		QueryKind = OptionQuery,467	>;468469	/// Collection properties470	#[pallet::storage]471	#[pallet::getter(fn collection_properties)]472	pub type CollectionProperties<T> = StorageMap<473		Hasher = Blake2_128Concat,474		Key = CollectionId,475		Value = Properties,476		QueryKind = ValueQuery,477		OnEmpty = up_data_structs::CollectionProperties,478	>;479480	#[pallet::storage]481	#[pallet::getter(fn property_permissions)]482	pub type CollectionPropertyPermissions<T> = StorageMap<483		Hasher = Blake2_128Concat,484		Key = CollectionId,485		Value = PropertiesPermissionMap,486		QueryKind = ValueQuery,487	>;488489	#[pallet::storage]490	pub type AdminAmount<T> = StorageMap<491		Hasher = Blake2_128Concat,492		Key = CollectionId,493		Value = u32,494		QueryKind = ValueQuery,495	>;496497	/// List of collection admins498	#[pallet::storage]499	pub type IsAdmin<T: Config> = StorageNMap<500		Key = (501			Key<Blake2_128Concat, CollectionId>,502			Key<Blake2_128Concat, T::CrossAccountId>,503		),504		Value = bool,505		QueryKind = ValueQuery,506	>;507508	/// Allowlisted collection users509	#[pallet::storage]510	pub type Allowlist<T: Config> = StorageNMap<511		Key = (512			Key<Blake2_128Concat, CollectionId>,513			Key<Blake2_128Concat, T::CrossAccountId>,514		),515		Value = bool,516		QueryKind = ValueQuery,517	>;518519	/// Not used by code, exists only to provide some types to metadata520	#[pallet::storage]521	pub type DummyStorageValue<T: Config> = StorageValue<522		Value = (523			CollectionStats,524			CollectionId,525			TokenId,526			TokenChild,527			PhantomType<(528				TokenData<T::CrossAccountId>,529				RpcCollection<T::AccountId>,530				// RMRK531				RmrkCollectionInfo<T::AccountId>,532				RmrkInstanceInfo<T::AccountId>,533				RmrkResourceInfo,534				RmrkPropertyInfo,535				RmrkBaseInfo<T::AccountId>,536				RmrkPartType,537				RmrkTheme,538				RmrkNftChild,539			)>,540		),541		QueryKind = OptionQuery,542	>;543544	#[pallet::hooks]545	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {546		fn on_runtime_upgrade() -> Weight {547			if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {548				use up_data_structs::{CollectionVersion1, CollectionVersion2};549				<CollectionById<T>>::translate::<CollectionVersion1<T::AccountId>, _>(|id, v| {550					let mut props = Vec::new();551					if !v.offchain_schema.is_empty() {552						props.push(Property {553							key: b"_old_offchainSchema".to_vec().try_into().unwrap(),554							value: v555								.offchain_schema556								.clone()557								.into_inner()558								.try_into()559								.expect("offchain schema too big"),560						});561					}562					if !v.variable_on_chain_schema.is_empty() {563						props.push(Property {564							key: b"_old_variableOnChainSchema".to_vec().try_into().unwrap(),565							value: v566								.variable_on_chain_schema567								.clone()568								.into_inner()569								.try_into()570								.expect("offchain schema too big"),571						});572					}573					if !v.const_on_chain_schema.is_empty() {574						props.push(Property {575							key: b"_old_constOnChainSchema".to_vec().try_into().unwrap(),576							value: v577								.const_on_chain_schema578								.clone()579								.into_inner()580								.try_into()581								.expect("offchain schema too big"),582						});583					}584					props.push(Property {585						key: b"_old_schemaVersion".to_vec().try_into().unwrap(),586						value: match v.schema_version {587							SchemaVersion::ImageURL => b"ImageUrl".as_slice(),588							SchemaVersion::Unique => b"Unique".as_slice(),589						}590						.to_vec()591						.try_into()592						.unwrap(),593					});594					Self::set_scoped_collection_properties(595						id,596						PropertyScope::None,597						props.into_iter(),598					)599					.expect("existing data larger than properties");600					let mut new = CollectionVersion2::from(v.clone());601					new.permissions.access = Some(v.access);602					new.permissions.mint_mode = Some(v.mint_mode);603					Some(new)604				});605			}606607			0608		}609	}610}611612impl<T: Config> Pallet<T> {613	/// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens614	pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {615		ensure!(616			&T::CrossAccountId::from_eth(H160([0; 20])) != receiver,617			<Error<T>>::AddressIsZero618		);619		Ok(())620	}621	pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {622		<IsAdmin<T>>::iter_prefix((collection,))623			.map(|(a, _)| a)624			.collect()625	}626	pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {627		<Allowlist<T>>::iter_prefix((collection,))628			.map(|(a, _)| a)629			.collect()630	}631	pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {632		<Allowlist<T>>::get((collection, user))633	}634	pub fn collection_stats() -> CollectionStats {635		let created = <CreatedCollectionCount<T>>::get();636		let destroyed = <DestroyedCollectionCount<T>>::get();637		CollectionStats {638			created: created.0,639			destroyed: destroyed.0,640			alive: created.0 - destroyed.0,641		}642	}643644	pub fn effective_collection_limits(collection: CollectionId) -> Option<CollectionLimits> {645		let collection = <CollectionById<T>>::get(collection);646		if collection.is_none() {647			return None;648		}649650		let collection = collection.unwrap();651		let limits = collection.limits;652		let effective_limits = CollectionLimits {653			account_token_ownership_limit: Some(limits.account_token_ownership_limit()),654			sponsored_data_size: Some(limits.sponsored_data_size()),655			sponsored_data_rate_limit: Some(656				limits657					.sponsored_data_rate_limit658					.unwrap_or(SponsoringRateLimit::SponsoringDisabled),659			),660			token_limit: Some(limits.token_limit()),661			sponsor_transfer_timeout: Some(limits.sponsor_transfer_timeout(662				match collection.mode {663					CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,664					CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,665					CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,666				},667			)),668			sponsor_approve_timeout: Some(limits.sponsor_approve_timeout()),669			owner_can_transfer: Some(limits.owner_can_transfer()),670			owner_can_destroy: Some(limits.owner_can_destroy()),671			transfers_enabled: Some(limits.transfers_enabled()),672		};673674		Some(effective_limits)675	}676677	pub fn rpc_collection(collection: CollectionId) -> Option<RpcCollection<T::AccountId>> {678		let Collection {679			name,680			description,681			owner,682			mode,683			token_prefix,684			sponsorship,685			limits,686			permissions,687			read_only,688		} = <CollectionById<T>>::get(collection)?;689690		let token_property_permissions = <CollectionPropertyPermissions<T>>::get(collection)691			.into_iter()692			.map(|(key, permission)| PropertyKeyPermission { key, permission })693			.collect();694695		let properties = <CollectionProperties<T>>::get(collection)696			.into_iter()697			.map(|(key, value)| Property { key, value })698			.collect();699700		let permissions = CollectionPermissions {701			access: Some(permissions.access()),702			mint_mode: Some(permissions.mint_mode()),703			nesting: Some(permissions.nesting().clone()),704		};705706		Some(RpcCollection {707			name: name.into_inner(),708			description: description.into_inner(),709			owner,710			mode,711			token_prefix: token_prefix.into_inner(),712			sponsorship,713			limits,714			permissions,715			token_property_permissions,716			properties,717			read_only,718		})719	}720}721722macro_rules! limit_default {723	($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{724		$(725			if let Some($new) = $new.$field {726				let $old = $old.$field($($arg)?);727				let _ = $new;728				let _ = $old;729				$check730			} else {731				$new.$field = $old.$field732			}733		)*734	}};735}736macro_rules! limit_default_clone {737	($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{738		$(739			if let Some($new) = $new.$field.clone() {740				let $old = $old.$field($($arg)?);741				let _ = $new;742				let _ = $old;743				$check744			} else {745				$new.$field = $old.$field.clone()746			}747		)*748	}};749}750751impl<T: Config> Pallet<T> {752	pub fn init_collection(753		owner: T::CrossAccountId,754		data: CreateCollectionData<T::AccountId>,755	) -> Result<CollectionId, DispatchError> {756		{757			ensure!(758				data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH as usize,759				Error::<T>::CollectionTokenPrefixLimitExceeded760			);761		}762763		let created_count = <CreatedCollectionCount<T>>::get()764			.0765			.checked_add(1)766			.ok_or(ArithmeticError::Overflow)?;767		let destroyed_count = <DestroyedCollectionCount<T>>::get().0;768		let id = CollectionId(created_count);769770		// bound Total number of collections771		ensure!(772			created_count - destroyed_count <= COLLECTION_NUMBER_LIMIT,773			<Error<T>>::TotalCollectionsLimitExceeded774		);775776		// =========777778		let collection = Collection {779			owner: owner.as_sub().clone(),780			name: data.name,781			mode: data.mode.clone(),782			description: data.description,783			token_prefix: data.token_prefix,784			sponsorship: data785				.pending_sponsor786				.map(SponsorshipState::Unconfirmed)787				.unwrap_or_default(),788			limits: data789				.limits790				.map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits))791				.unwrap_or_else(|| Ok(CollectionLimits::default()))?,792			permissions: data793				.permissions794				.map(|permissions| {795					Self::clamp_permissions(data.mode.clone(), &Default::default(), permissions)796				})797				.unwrap_or_else(|| Ok(CollectionPermissions::default()))?,798			read_only: false,799		};800801		let mut collection_properties = up_data_structs::CollectionProperties::get();802		collection_properties803			.try_set_from_iter(data.properties.into_iter())804			.map_err(<Error<T>>::from)?;805806		CollectionProperties::<T>::insert(id, collection_properties);807808		let mut token_props_permissions = PropertiesPermissionMap::new();809		token_props_permissions810			.try_set_from_iter(data.token_property_permissions.into_iter())811			.map_err(<Error<T>>::from)?;812813		CollectionPropertyPermissions::<T>::insert(id, token_props_permissions);814815		// Take a (non-refundable) deposit of collection creation816		{817			let mut imbalance =818				<<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();819			imbalance.subsume(820				<<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(821					&T::TreasuryAccountId::get(),822					T::CollectionCreationPrice::get(),823				),824			);825			<T as Config>::Currency::settle(826				&owner.as_sub(),827				imbalance,828				WithdrawReasons::TRANSFER,829				ExistenceRequirement::KeepAlive,830			)831			.map_err(|_| Error::<T>::NotSufficientFounds)?;832		}833834		<CreatedCollectionCount<T>>::put(created_count);835		<Pallet<T>>::deposit_event(Event::CollectionCreated(836			id,837			data.mode.id(),838			owner.as_sub().clone(),839		));840		<PalletEvm<T>>::deposit_log(841			erc::CollectionHelpersEvents::CollectionCreated {842				owner: *owner.as_eth(),843				collection_id: eth::collection_id_to_address(id),844			}845			.to_log(T::ContractAddress::get()),846		);847		<CollectionById<T>>::insert(id, collection);848		Ok(id)849	}850851	pub fn destroy_collection(852		collection: CollectionHandle<T>,853		sender: &T::CrossAccountId,854	) -> DispatchResult {855		collection.check_is_read_only()?;856		ensure!(857			collection.limits.owner_can_destroy(),858			<Error<T>>::NoPermission,859		);860		collection.check_is_owner(sender)?;861862		let destroyed_collections = <DestroyedCollectionCount<T>>::get()863			.0864			.checked_add(1)865			.ok_or(ArithmeticError::Overflow)?;866867		// =========868869		<DestroyedCollectionCount<T>>::put(destroyed_collections);870		<CollectionById<T>>::remove(collection.id);871		<AdminAmount<T>>::remove(collection.id);872		<IsAdmin<T>>::remove_prefix((collection.id,), None);873		<Allowlist<T>>::remove_prefix((collection.id,), None);874		<CollectionProperties<T>>::remove(collection.id);875876		<Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));877		Ok(())878	}879880	pub fn set_collection_property(881		collection: &CollectionHandle<T>,882		sender: &T::CrossAccountId,883		property: Property,884	) -> DispatchResult {885		collection.check_is_read_only()?;886		collection.check_is_owner_or_admin(sender)?;887888		CollectionProperties::<T>::try_mutate(collection.id, |properties| {889			let property = property.clone();890			properties.try_set(property.key, property.value)891		})892		.map_err(<Error<T>>::from)?;893894		Self::deposit_event(Event::CollectionPropertySet(collection.id, property.key));895896		Ok(())897	}898899	pub fn set_scoped_collection_property(900		collection_id: CollectionId,901		scope: PropertyScope,902		property: Property,903	) -> DispatchResult {904		CollectionProperties::<T>::try_mutate(collection_id, |properties| {905			properties.try_scoped_set(scope, property.key, property.value)906		})907		.map_err(<Error<T>>::from)?;908909		Ok(())910	}911912	pub fn set_scoped_collection_properties(913		collection_id: CollectionId,914		scope: PropertyScope,915		properties: impl Iterator<Item = Property>,916	) -> DispatchResult {917		CollectionProperties::<T>::try_mutate(collection_id, |stored_properties| {918			stored_properties.try_scoped_set_from_iter(scope, properties)919		})920		.map_err(<Error<T>>::from)?;921922		Ok(())923	}924925	#[transactional]926	pub fn set_collection_properties(927		collection: &CollectionHandle<T>,928		sender: &T::CrossAccountId,929		properties: Vec<Property>,930	) -> DispatchResult {931		collection.check_is_read_only()?;932933		for property in properties {934			Self::set_collection_property(collection, sender, property)?;935		}936937		Ok(())938	}939940	pub fn delete_collection_property(941		collection: &CollectionHandle<T>,942		sender: &T::CrossAccountId,943		property_key: PropertyKey,944	) -> DispatchResult {945		collection.check_is_read_only()?;946		collection.check_is_owner_or_admin(sender)?;947948		CollectionProperties::<T>::try_mutate(collection.id, |properties| {949			properties.remove(&property_key)950		})951		.map_err(<Error<T>>::from)?;952953		Self::deposit_event(Event::CollectionPropertyDeleted(954			collection.id,955			property_key,956		));957958		Ok(())959	}960961	#[transactional]962	pub fn delete_collection_properties(963		collection: &CollectionHandle<T>,964		sender: &T::CrossAccountId,965		property_keys: Vec<PropertyKey>,966	) -> DispatchResult {967		collection.check_is_read_only()?;968969		for key in property_keys {970			Self::delete_collection_property(collection, sender, key)?;971		}972973		Ok(())974	}975976	// For migrations977	pub fn set_property_permission_unchecked(978		collection: CollectionId,979		property_permission: PropertyKeyPermission,980	) -> DispatchResult {981		<CollectionPropertyPermissions<T>>::try_mutate(collection, |permissions| {982			permissions.try_set(property_permission.key, property_permission.permission)983		})984		.map_err(<Error<T>>::from)?;985		Ok(())986	}987988	pub fn set_property_permission(989		collection: &CollectionHandle<T>,990		sender: &T::CrossAccountId,991		property_permission: PropertyKeyPermission,992	) -> DispatchResult {993		collection.check_is_read_only()?;994		collection.check_is_owner_or_admin(sender)?;995996		let all_permissions = CollectionPropertyPermissions::<T>::get(collection.id);997		let current_permission = all_permissions.get(&property_permission.key);998		if matches![999			current_permission,1000			Some(PropertyPermission { mutable: false, .. })1001		] {1002			return Err(<Error<T>>::NoPermission.into());1003		}10041005		CollectionPropertyPermissions::<T>::try_mutate(collection.id, |permissions| {1006			let property_permission = property_permission.clone();1007			permissions.try_set(property_permission.key, property_permission.permission)1008		})1009		.map_err(<Error<T>>::from)?;10101011		Self::deposit_event(Event::PropertyPermissionSet(1012			collection.id,1013			property_permission.key,1014		));10151016		Ok(())1017	}10181019	#[transactional]1020	pub fn set_property_permissions(1021		collection: &CollectionHandle<T>,1022		sender: &T::CrossAccountId,1023		property_permissions: Vec<PropertyKeyPermission>,1024	) -> DispatchResult {1025		collection.check_is_read_only()?;10261027		for prop_pemission in property_permissions {1028			Self::set_property_permission(collection, sender, prop_pemission)?;1029		}10301031		Ok(())1032	}10331034	pub fn get_collection_property(1035		collection_id: CollectionId,1036		key: &PropertyKey,1037	) -> Option<PropertyValue> {1038		Self::collection_properties(collection_id).get(key).cloned()1039	}10401041	pub fn bytes_keys_to_property_keys(1042		keys: Vec<Vec<u8>>,1043	) -> Result<Vec<PropertyKey>, DispatchError> {1044		keys.into_iter()1045			.map(|key| -> Result<PropertyKey, DispatchError> {1046				key.try_into()1047					.map_err(|_| <Error<T>>::PropertyKeyIsTooLong.into())1048			})1049			.collect::<Result<Vec<PropertyKey>, DispatchError>>()1050	}10511052	pub fn filter_collection_properties(1053		collection_id: CollectionId,1054		keys: Option<Vec<PropertyKey>>,1055	) -> Result<Vec<Property>, DispatchError> {1056		let properties = Self::collection_properties(collection_id);10571058		let properties = keys1059			.map(|keys| {1060				keys.into_iter()1061					.filter_map(|key| {1062						properties.get(&key).map(|value| Property {1063							key,1064							value: value.clone(),1065						})1066					})1067					.collect()1068			})1069			.unwrap_or_else(|| {1070				properties1071					.into_iter()1072					.map(|(key, value)| Property { key, value })1073					.collect()1074			});10751076		Ok(properties)1077	}10781079	pub fn filter_property_permissions(1080		collection_id: CollectionId,1081		keys: Option<Vec<PropertyKey>>,1082	) -> Result<Vec<PropertyKeyPermission>, DispatchError> {1083		let permissions = Self::property_permissions(collection_id);10841085		let key_permissions = keys1086			.map(|keys| {1087				keys.into_iter()1088					.filter_map(|key| {1089						permissions1090							.get(&key)1091							.map(|permission| PropertyKeyPermission {1092								key,1093								permission: permission.clone(),1094							})1095					})1096					.collect()1097			})1098			.unwrap_or_else(|| {1099				permissions1100					.into_iter()1101					.map(|(key, permission)| PropertyKeyPermission { key, permission })1102					.collect()1103			});11041105		Ok(key_permissions)1106	}11071108	pub fn toggle_allowlist(1109		collection: &CollectionHandle<T>,1110		sender: &T::CrossAccountId,1111		user: &T::CrossAccountId,1112		allowed: bool,1113	) -> DispatchResult {1114		collection.check_is_read_only()?;1115		collection.check_is_owner_or_admin(sender)?;11161117		// =========11181119		if allowed {1120			<Allowlist<T>>::insert((collection.id, user), true);1121		} else {1122			<Allowlist<T>>::remove((collection.id, user));1123		}11241125		Ok(())1126	}11271128	pub fn toggle_admin(1129		collection: &CollectionHandle<T>,1130		sender: &T::CrossAccountId,1131		user: &T::CrossAccountId,1132		admin: bool,1133	) -> DispatchResult {1134		collection.check_is_read_only()?;1135		collection.check_is_owner_or_admin(sender)?;11361137		let was_admin = <IsAdmin<T>>::get((collection.id, user));1138		if was_admin == admin {1139			return Ok(());1140		}1141		let amount = <AdminAmount<T>>::get(collection.id);11421143		if admin {1144			let amount = amount1145				.checked_add(1)1146				.ok_or(<Error<T>>::CollectionAdminCountExceeded)?;1147			ensure!(1148				amount <= Self::collection_admins_limit(),1149				<Error<T>>::CollectionAdminCountExceeded,1150			);11511152			// =========11531154			<AdminAmount<T>>::insert(collection.id, amount);1155			<IsAdmin<T>>::insert((collection.id, user), true);1156		} else {1157			<AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));1158			<IsAdmin<T>>::remove((collection.id, user));1159		}11601161		Ok(())1162	}11631164	pub fn clamp_limits(1165		mode: CollectionMode,1166		old_limit: &CollectionLimits,1167		mut new_limit: CollectionLimits,1168	) -> Result<CollectionLimits, DispatchError> {1169		limit_default!(old_limit, new_limit,1170			account_token_ownership_limit => ensure!(1171				new_limit <= MAX_TOKEN_OWNERSHIP,1172				<Error<T>>::CollectionLimitBoundsExceeded,1173			),1174			sponsored_data_size => ensure!(1175				new_limit <= CUSTOM_DATA_LIMIT,1176				<Error<T>>::CollectionLimitBoundsExceeded,1177			),11781179			sponsored_data_rate_limit => {},1180			token_limit => ensure!(1181				old_limit >= new_limit && new_limit > 0,1182				<Error<T>>::CollectionTokenLimitExceeded1183			),11841185			sponsor_transfer_timeout(match mode {1186				CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,1187				CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,1188				CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,1189			}) => ensure!(1190				new_limit <= MAX_SPONSOR_TIMEOUT,1191				<Error<T>>::CollectionLimitBoundsExceeded,1192			),1193			sponsor_approve_timeout => {},1194			owner_can_transfer => ensure!(1195				old_limit || !new_limit,1196				<Error<T>>::OwnerPermissionsCantBeReverted,1197			),1198			owner_can_destroy => ensure!(1199				old_limit || !new_limit,1200				<Error<T>>::OwnerPermissionsCantBeReverted,1201			),1202			transfers_enabled => {},1203		);1204		Ok(new_limit)1205	}12061207	pub fn clamp_permissions(1208		_mode: CollectionMode,1209		old_limit: &CollectionPermissions,1210		mut new_limit: CollectionPermissions,1211	) -> Result<CollectionPermissions, DispatchError> {1212		limit_default_clone!(old_limit, new_limit,1213			access => {},1214			mint_mode => {},1215			nesting => {},1216		);1217		Ok(new_limit)1218	}1219}12201221#[macro_export]1222macro_rules! unsupported {1223	() => {1224		Err(<Error<T>>::UnsupportedOperation.into())1225	};1226}12271228/// Worst cases1229pub trait CommonWeightInfo<CrossAccountId> {1230	fn create_item() -> Weight;1231	fn create_multiple_items(amount: &[CreateItemData]) -> Weight;1232	fn create_multiple_items_ex(cost: &CreateItemExData<CrossAccountId>) -> Weight;1233	fn burn_item() -> Weight;1234	fn set_collection_properties(amount: u32) -> Weight;1235	fn delete_collection_properties(amount: u32) -> Weight;1236	fn set_token_properties(amount: u32) -> Weight;1237	fn delete_token_properties(amount: u32) -> Weight;1238	fn set_property_permissions(amount: u32) -> Weight;1239	fn transfer() -> Weight;1240	fn approve() -> Weight;1241	fn transfer_from() -> Weight;1242	fn burn_from() -> Weight;12431244	/// Differs from burn_item in case of Fungible and Refungible, as it should burn1245	/// whole users's balance1246	///1247	/// This method shouldn't be used directly, as it doesn't count breadth price, use `burn_recursively` instead1248	fn burn_recursively_self_raw() -> Weight;1249	/// Cost of iterating over `amount` children while burning, without counting child burning itself1250	///1251	/// This method shouldn't be used directly, as it doesn't count depth price, use `burn_recursively` instead1252	fn burn_recursively_breadth_raw(amount: u32) -> Weight;12531254	fn burn_recursively(max_selfs: u32, max_breadth: u32) -> Weight {1255		Self::burn_recursively_self_raw()1256			.saturating_mul(max_selfs.max(1) as u64)1257			.saturating_add(Self::burn_recursively_breadth_raw(max_breadth))1258	}1259}12601261pub trait CommonCollectionOperations<T: Config> {1262	fn create_item(1263		&self,1264		sender: T::CrossAccountId,1265		to: T::CrossAccountId,1266		data: CreateItemData,1267		nesting_budget: &dyn Budget,1268	) -> DispatchResultWithPostInfo;1269	fn create_multiple_items(1270		&self,1271		sender: T::CrossAccountId,1272		to: T::CrossAccountId,1273		data: Vec<CreateItemData>,1274		nesting_budget: &dyn Budget,1275	) -> DispatchResultWithPostInfo;1276	fn create_multiple_items_ex(1277		&self,1278		sender: T::CrossAccountId,1279		data: CreateItemExData<T::CrossAccountId>,1280		nesting_budget: &dyn Budget,1281	) -> DispatchResultWithPostInfo;1282	fn burn_item(1283		&self,1284		sender: T::CrossAccountId,1285		token: TokenId,1286		amount: u128,1287	) -> DispatchResultWithPostInfo;1288	fn burn_item_recursively(1289		&self,1290		sender: T::CrossAccountId,1291		token: TokenId,1292		self_budget: &dyn Budget,1293		breadth_budget: &dyn Budget,1294	) -> DispatchResultWithPostInfo;1295	fn set_collection_properties(1296		&self,1297		sender: T::CrossAccountId,1298		properties: Vec<Property>,1299	) -> DispatchResultWithPostInfo;1300	fn delete_collection_properties(1301		&self,1302		sender: &T::CrossAccountId,1303		property_keys: Vec<PropertyKey>,1304	) -> DispatchResultWithPostInfo;1305	fn set_token_properties(1306		&self,1307		sender: T::CrossAccountId,1308		token_id: TokenId,1309		property: Vec<Property>,1310	) -> DispatchResultWithPostInfo;1311	fn delete_token_properties(1312		&self,1313		sender: T::CrossAccountId,1314		token_id: TokenId,1315		property_keys: Vec<PropertyKey>,1316	) -> DispatchResultWithPostInfo;1317	fn set_property_permissions(1318		&self,1319		sender: &T::CrossAccountId,1320		property_permissions: Vec<PropertyKeyPermission>,1321	) -> DispatchResultWithPostInfo;1322	fn transfer(1323		&self,1324		sender: T::CrossAccountId,1325		to: T::CrossAccountId,1326		token: TokenId,1327		amount: u128,1328		nesting_budget: &dyn Budget,1329	) -> DispatchResultWithPostInfo;1330	fn approve(1331		&self,1332		sender: T::CrossAccountId,1333		spender: T::CrossAccountId,1334		token: TokenId,1335		amount: u128,1336	) -> DispatchResultWithPostInfo;1337	fn transfer_from(1338		&self,1339		sender: T::CrossAccountId,1340		from: T::CrossAccountId,1341		to: T::CrossAccountId,1342		token: TokenId,1343		amount: u128,1344		nesting_budget: &dyn Budget,1345	) -> DispatchResultWithPostInfo;1346	fn burn_from(1347		&self,1348		sender: T::CrossAccountId,1349		from: T::CrossAccountId,1350		token: TokenId,1351		amount: u128,1352		nesting_budget: &dyn Budget,1353	) -> DispatchResultWithPostInfo;13541355	fn check_nesting(1356		&self,1357		sender: T::CrossAccountId,1358		from: (CollectionId, TokenId),1359		under: TokenId,1360		budget: &dyn Budget,1361	) -> DispatchResult;13621363	fn nest(&self, under: TokenId, to_nest: (CollectionId, TokenId));13641365	fn unnest(&self, under: TokenId, to_nest: (CollectionId, TokenId));13661367	fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;1368	fn collection_tokens(&self) -> Vec<TokenId>;1369	fn token_exists(&self, token: TokenId) -> bool;1370	fn last_token_id(&self) -> TokenId;13711372	fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId>;1373	fn token_property(&self, token_id: TokenId, key: &PropertyKey) -> Option<PropertyValue>;1374	fn token_properties(&self, token_id: TokenId, keys: Option<Vec<PropertyKey>>) -> Vec<Property>;1375	/// Amount of unique collection tokens1376	fn total_supply(&self) -> u32;1377	/// Amount of different tokens account has (Applicable to nonfungible/refungible)1378	fn account_balance(&self, account: T::CrossAccountId) -> u32;1379	/// Amount of specific token account have (Applicable to fungible/refungible)1380	fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;1381	fn allowance(1382		&self,1383		sender: T::CrossAccountId,1384		spender: T::CrossAccountId,1385		token: TokenId,1386	) -> u128;1387}13881389// Flexible enough for implementing CommonCollectionOperations1390pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {1391	let post_info = PostDispatchInfo {1392		actual_weight: Some(weight),1393		pays_fee: Pays::Yes,1394	};1395	match res {1396		Ok(()) => Ok(post_info),1397		Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),1398	}1399}14001401impl<T: Config> From<PropertiesError> for Error<T> {1402	fn from(error: PropertiesError) -> Self {1403		match error {1404			PropertiesError::NoSpaceForProperty => Self::NoSpaceForProperty,1405			PropertiesError::PropertyLimitReached => Self::PropertyLimitReached,1406			PropertiesError::InvalidCharacterInPropertyKey => Self::InvalidCharacterInPropertyKey,1407			PropertiesError::PropertyKeyIsTooLong => Self::PropertyKeyIsTooLong,1408			PropertiesError::EmptyPropertyKey => Self::EmptyPropertyKey,1409		}1410	}1411}
modifiedpallets/fungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/fungible/src/lib.rs
+++ b/pallets/fungible/src/lib.rs
@@ -168,6 +168,8 @@
 		owner: &T::CrossAccountId,
 		amount: u128,
 	) -> DispatchResult {
+		collection.check_is_read_only()?;
+
 		let total_supply = <TotalSupply<T>>::get(collection.id)
 			.checked_sub(amount)
 			.ok_or(<CommonError<T>>::TokenValueTooLow)?;
@@ -214,6 +216,8 @@
 		amount: u128,
 		nesting_budget: &dyn Budget,
 	) -> DispatchResult {
+		collection.check_is_read_only()?;
+
 		ensure!(
 			collection.limits.transfers_enabled(),
 			<CommonError<T>>::TransferNotAllowed,
@@ -283,6 +287,8 @@
 		data: BTreeMap<T::CrossAccountId, u128>,
 		nesting_budget: &dyn Budget,
 	) -> DispatchResult {
+		collection.check_is_read_only()?;
+
 		if !collection.is_owner_or_admin(sender) {
 			ensure!(
 				collection.permissions.mint_mode(),
@@ -384,6 +390,7 @@
 		spender: &T::CrossAccountId,
 		amount: u128,
 	) -> DispatchResult {
+		collection.check_is_read_only()?;
 		if collection.permissions.access() == AccessMode::AllowList {
 			collection.check_allowlist(owner)?;
 			collection.check_allowlist(spender)?;
modifiedpallets/nonfungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/lib.rs
+++ b/pallets/nonfungible/src/lib.rs
@@ -336,6 +336,8 @@
 		sender: &T::CrossAccountId,
 		token: TokenId,
 	) -> DispatchResult {
+		collection.check_is_read_only()?;
+
 		let token_data =
 			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;
 		ensure!(
@@ -456,6 +458,7 @@
 			&property.key,
 			is_token_create,
 		)?;
+		collection.check_is_read_only()?;
 
 		<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {
 			let property = property.clone();
@@ -494,6 +497,7 @@
 		property_key: PropertyKey,
 	) -> DispatchResult {
 		Self::check_token_change_permission(collection, sender, token_id, &property_key, false)?;
+		collection.check_is_read_only()?;
 
 		<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {
 			properties.remove(&property_key)
@@ -570,6 +574,8 @@
 		token_id: TokenId,
 		property_keys: Vec<PropertyKey>,
 	) -> DispatchResult {
+		collection.check_is_read_only()?;
+
 		for key in property_keys {
 			Self::delete_token_property(collection, sender, token_id, key)?;
 		}
@@ -616,6 +622,8 @@
 		token: TokenId,
 		nesting_budget: &dyn Budget,
 	) -> DispatchResult {
+		collection.check_is_read_only()?;
+
 		ensure!(
 			collection.limits.transfers_enabled(),
 			<CommonError<T>>::TransferNotAllowed
@@ -894,6 +902,8 @@
 		token: TokenId,
 		spender: Option<&T::CrossAccountId>,
 	) -> DispatchResult {
+		collection.check_is_read_only()?;
+		
 		if collection.permissions.access() == AccessMode::AllowList {
 			collection.check_allowlist(sender)?;
 			if let Some(spender) = spender {
modifiedpallets/refungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -234,6 +234,7 @@
 	}
 
 	pub fn burn_token(collection: &RefungibleHandle<T>, token_id: TokenId) -> DispatchResult {
+		collection.check_is_read_only()?;
 		let burnt = <TokensBurnt<T>>::get(collection.id)
 			.checked_add(1)
 			.ok_or(ArithmeticError::Overflow)?;
@@ -253,6 +254,7 @@
 		token: TokenId,
 		amount: u128,
 	) -> DispatchResult {
+		collection.check_is_read_only()?;
 		let total_supply = <TotalSupply<T>>::get((collection.id, token))
 			.checked_sub(amount)
 			.ok_or(<CommonError<T>>::TokenValueTooLow)?;
@@ -325,6 +327,7 @@
 		amount: u128,
 		nesting_budget: &dyn Budget,
 	) -> DispatchResult {
+		collection.check_is_read_only()?;
 		ensure!(
 			collection.limits.transfers_enabled(),
 			<CommonError<T>>::TransferNotAllowed
@@ -573,6 +576,7 @@
 		token: TokenId,
 		amount: u128,
 	) -> DispatchResult {
+		collection.check_is_read_only()?;
 		if collection.permissions.access() == AccessMode::AllowList {
 			collection.check_allowlist(sender)?;
 			collection.check_allowlist(spender)?;
modifiedpallets/unique/src/lib.rsdiffbeforeafterboth
--- a/pallets/unique/src/lib.rs
+++ b/pallets/unique/src/lib.rs
@@ -304,6 +304,7 @@
 		pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
 			let collection = <CollectionHandle<T>>::try_get(collection_id)?;
+			collection.check_is_read_only()?;
 
 			// =========
 
@@ -406,6 +407,7 @@
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
 
 			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;
+			target_collection.check_is_read_only()?;
 			target_collection.check_is_owner(&sender)?;
 
 			target_collection.owner = new_owner.clone();
@@ -487,7 +489,7 @@
 			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;
 			target_collection.check_is_owner(&sender)?;
 
-			target_collection.set_sponsor(new_sponsor.clone());
+			target_collection.set_sponsor(new_sponsor.clone())?;
 
 			<Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(
 				collection_id,
@@ -511,7 +513,7 @@
 
 			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;
 			ensure!(
-				target_collection.confirm_sponsorship(&sender),
+				target_collection.confirm_sponsorship(&sender)?,
 				Error::<T>::ConfirmUnsetSponsorFail
 			);
 
modifiedprimitives/data-structs/src/lib.rsdiffbeforeafterboth
--- a/primitives/data-structs/src/lib.rs
+++ b/primitives/data-structs/src/lib.rs
@@ -316,6 +316,9 @@
 	#[version(2.., upper(Default::default()))]
 	pub permissions: CollectionPermissions,
 
+	#[version(2.., upper(false))]
+	pub read_only: bool,
+
 	#[version(..2)]
 	pub variable_on_chain_schema: BoundedVec<u8, ConstU32<VARIABLE_ON_CHAIN_SCHEMA_LIMIT>>,
 
@@ -340,6 +343,7 @@
 	pub permissions: CollectionPermissions,
 	pub token_property_permissions: Vec<PropertyKeyPermission>,
 	pub properties: Vec<Property>,
+	pub read_only: bool,
 }
 
 #[derive(Encode, Decode, Clone, PartialEq, TypeInfo, Derivative, MaxEncodedLen)]
modifiedtests/src/createCollection.test.tsdiffbeforeafterboth
--- a/tests/src/createCollection.test.ts
+++ b/tests/src/createCollection.test.ts
@@ -87,6 +87,20 @@
       expect(collection.limits.accountTokenOwnershipLimit.unwrap().toNumber()).to.equal(3);
     });
   });
+
+  it('Create new collection is not read only', async () => {
+    await usingApi(async api => {
+      const alice = privateKey('//Alice');
+      const tx = api.tx.unique.createCollectionEx({
+        readOnly: true
+      });
+      const events = await submitTransactionAsync(alice, tx);
+      const result = getCreateCollectionResult(events);
+
+      const collection = (await getDetailedCollectionInfo(api, result.collectionId))!;
+      expect(collection.readOnly.toHuman()).to.be.false;
+    });
+  });
 });
 
 describe('(!negative test!) integration test: ext. createCollection():', () => {