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

difftreelog

feat track and limit admin amount

Yaroslav Bolyukin2021-11-04parent: #98884c8.patch.diff
in: master

3 files changed

modifiedpallets/common/src/lib.rsdiffbeforeafterboth
after · pallets/common/src/lib.rs
1#![cfg_attr(not(feature = "std"), no_std)]23use core::ops::{Deref, DerefMut};4use sp_std::vec::Vec;5use account::CrossAccountId;6use frame_support::{7	dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo},8	ensure, fail,9	traits::{Imbalance, Get, Currency},10};11use nft_data_structs::{12	COLLECTION_NUMBER_LIMIT, Collection, CollectionId, CreateItemData, ExistenceRequirement,13	MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_COLLECTION_NAME_LENGTH, MAX_TOKEN_PREFIX_LENGTH,14	COLLECTION_ADMINS_LIMIT, MetaUpdatePermission, Pays, PostDispatchInfo, TokenId, Weight,15	WithdrawReasons,16};17pub use pallet::*;18use sp_core::H160;19use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};20pub mod account;21#[cfg(feature = "runtime-benchmarks")]22pub mod benchmarking;23pub mod erc;24pub mod eth;2526#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]27pub struct CollectionHandle<T: Config> {28	pub id: CollectionId,29	collection: Collection<T>,30	pub recorder: pallet_evm_coder_substrate::SubstrateRecorder<T>,31}32impl<T: Config> CollectionHandle<T> {33	pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {34		<CollectionById<T>>::get(id).map(|collection| Self {35			id,36			collection,37			recorder: pallet_evm_coder_substrate::SubstrateRecorder::new(38				eth::collection_id_to_address(id),39				gas_limit,40			),41		})42	}43	pub fn new(id: CollectionId) -> Option<Self> {44		Self::new_with_gas_limit(id, u64::MAX)45	}46	pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {47		Ok(Self::new(id).ok_or_else(|| <Error<T>>::CollectionNotFound)?)48	}49	pub fn log(&self, log: impl evm_coder::ToLog) -> DispatchResult {50		self.recorder.log_sub(log)51	}52	pub fn log_infallible(&self, log: impl evm_coder::ToLog) {53		self.recorder.log_infallible(log)54	}55	#[allow(dead_code)]56	fn consume_gas(&self, gas: u64) -> DispatchResult {57		self.recorder.consume_gas_sub(gas)58	}59	pub fn consume_sload(&self) -> DispatchResult {60		self.recorder.consume_sload_sub()61	}62	pub fn consume_sstores(&self, amount: usize) -> DispatchResult {63		self.recorder.consume_sstores_sub(amount)64	}65	pub fn consume_sstore(&self) -> DispatchResult {66		self.recorder.consume_sstore_sub()67	}68	pub fn consume_log(&self, topics: usize, data: usize) -> DispatchResult {69		self.recorder.consume_log_sub(topics, data)70	}71	pub fn submit_logs(self) -> DispatchResult {72		self.recorder.submit_logs()73	}74	pub fn save(self) -> DispatchResult {75		self.recorder.submit_logs()?;76		<CollectionById<T>>::insert(self.id, self.collection);77		Ok(())78	}79}80impl<T: Config> Deref for CollectionHandle<T> {81	type Target = Collection<T>;8283	fn deref(&self) -> &Self::Target {84		&self.collection85	}86}8788impl<T: Config> DerefMut for CollectionHandle<T> {89	fn deref_mut(&mut self) -> &mut Self::Target {90		&mut self.collection91	}92}9394impl<T: Config> CollectionHandle<T> {95	pub fn check_is_owner(&self, subject: &T::CrossAccountId) -> DispatchResult {96		ensure!(*subject.as_sub() == self.owner, <Error<T>>::NoPermission);97		Ok(())98	}99	pub fn is_owner_or_admin(&self, subject: &T::CrossAccountId) -> Result<bool, DispatchError> {100		self.consume_sload()?;101102		Ok(*subject.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, subject)))103	}104	pub fn check_is_owner_or_admin(&self, subject: &T::CrossAccountId) -> DispatchResult {105		ensure!(self.is_owner_or_admin(subject)?, <Error<T>>::NoPermission);106		Ok(())107	}108	pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> Result<bool, DispatchError> {109		Ok(self.limits.owner_can_transfer() && self.is_owner_or_admin(user)?)110	}111	pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> Result<bool, DispatchError> {112		Ok(self.limits.owner_can_transfer() && self.is_owner_or_admin(user)?)113	}114	pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {115		self.consume_sload()?;116117		ensure!(118			<Allowlist<T>>::get((self.id, user)),119			<Error<T>>::AddressNotInAllowlist120		);121		Ok(())122	}123124	pub fn check_can_update_meta(125		&self,126		subject: &T::CrossAccountId,127		item_owner: &T::CrossAccountId,128	) -> DispatchResult {129		match self.meta_update_permission {130			MetaUpdatePermission::ItemOwner => {131				ensure!(subject == item_owner, <Error<T>>::NoPermission);132				Ok(())133			}134			MetaUpdatePermission::Admin => self.check_is_owner_or_admin(subject),135			MetaUpdatePermission::None => fail!(<Error<T>>::NoPermission),136		}137	}138}139140#[frame_support::pallet]141pub mod pallet {142	use super::*;143	use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key};144	use account::{EvmBackwardsAddressMapping, CrossAccountId};145	use frame_support::traits::Currency;146	use nft_data_structs::TokenId;147	use scale_info::TypeInfo;148149	#[pallet::config]150	pub trait Config: frame_system::Config + pallet_evm_coder_substrate::Config + TypeInfo {151		type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;152153		type CrossAccountId: CrossAccountId<Self::AccountId>;154155		type EvmAddressMapping: pallet_evm::AddressMapping<Self::AccountId>;156		type EvmBackwardsAddressMapping: EvmBackwardsAddressMapping<Self::AccountId>;157158		type Currency: Currency<Self::AccountId>;159		type CollectionCreationPrice: Get<160			<<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,161		>;162		type TreasuryAccountId: Get<Self::AccountId>;163	}164165	#[pallet::pallet]166	#[pallet::generate_store(pub(super) trait Store)]167	pub struct Pallet<T>(_);168169	#[pallet::extra_constants]170	impl<T: Config> Pallet<T> {171		pub fn collection_admins_limit() -> u32 {172			COLLECTION_ADMINS_LIMIT173		}174	}175176	#[pallet::event]177	#[pallet::generate_deposit(pub fn deposit_event)]178	pub enum Event<T: Config> {179		/// New collection was created180		///181		/// # Arguments182		///183		/// * collection_id: Globally unique identifier of newly created collection.184		///185		/// * mode: [CollectionMode] converted into u8.186		///187		/// * account_id: Collection owner.188		CollectionCreated(CollectionId, u8, T::AccountId),189190		/// New item was created.191		///192		/// # Arguments193		///194		/// * collection_id: Id of the collection where item was created.195		///196		/// * item_id: Id of an item. Unique within the collection.197		///198		/// * recipient: Owner of newly created item199		///200		/// * amount: Always 1 for NFT201		ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128),202203		/// Collection item was burned.204		///205		/// # Arguments206		///207		/// * collection_id.208		///209		/// * item_id: Identifier of burned NFT.210		///211		/// * owner: which user has destroyed its tokens212		///213		/// * amount: Always 1 for NFT214		ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128),215216		/// Item was transferred217		///218		/// * collection_id: Id of collection to which item is belong219		///220		/// * item_id: Id of an item221		///222		/// * sender: Original owner of item223		///224		/// * recipient: New owner of item225		///226		/// * amount: Always 1 for NFT227		Transfer(228			CollectionId,229			TokenId,230			T::CrossAccountId,231			T::CrossAccountId,232			u128,233		),234235		/// * collection_id236		///237		/// * item_id238		///239		/// * sender240		///241		/// * spender242		///243		/// * amount244		Approved(245			CollectionId,246			TokenId,247			T::CrossAccountId,248			T::CrossAccountId,249			u128,250		),251	}252253	#[pallet::error]254	pub enum Error<T> {255		/// This collection does not exist.256		CollectionNotFound,257		/// Sender parameter and item owner must be equal.258		MustBeTokenOwner,259		/// No permission to perform action260		NoPermission,261		/// Collection is not in mint mode.262		PublicMintingNotAllowed,263		/// Address is not in white list.264		AddressNotInAllowlist,265266		/// Collection name can not be longer than 63 char.267		CollectionNameLimitExceeded,268		/// Collection description can not be longer than 255 char.269		CollectionDescriptionLimitExceeded,270		/// Token prefix can not be longer than 15 char.271		CollectionTokenPrefixLimitExceeded,272		/// Total collections bound exceeded.273		TotalCollectionsLimitExceeded,274		/// variable_data exceeded data limit.275		TokenVariableDataLimitExceeded,276		/// Exceeded max admin amount277		CollectionAdminAmountExceeded,278279		/// Collection settings not allowing items transferring280		TransferNotAllowed,281		/// Account token limit exceeded per collection282		AccountTokenLimitExceeded,283		/// Collection token limit exceeded284		CollectionTokenLimitExceeded,285		/// Metadata flag frozen286		MetadataFlagFrozen,287288		/// Item not exists.289		TokenNotFound,290		/// Item balance not enough.291		TokenValueTooLow,292		/// Requested value more than approved.293		TokenValueNotEnough,294		/// Tried to approve more than owned295		CantApproveMoreThanOwned,296297		/// Can't transfer tokens to ethereum zero address298		AddressIsZero,299		/// Target collection doesn't supports this operation300		UnsupportedOperation,301	}302303	#[pallet::storage]304	pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;305	#[pallet::storage]306	pub type DestroyedCollectionCount<T> =307		StorageValue<Value = CollectionId, QueryKind = ValueQuery>;308309	/// Collection info310	#[pallet::storage]311	pub type CollectionById<T> = StorageMap<312		Hasher = Blake2_128Concat,313		Key = CollectionId,314		Value = Collection<T>,315		QueryKind = OptionQuery,316	>;317318	#[pallet::storage]319	pub type AdminAmount<T> = StorageMap<320		Hasher = Blake2_128Concat,321		Key = CollectionId,322		Value = u32,323		QueryKind = ValueQuery,324	>;325326	/// List of collection admins327	#[pallet::storage]328	pub type IsAdmin<T: Config> = StorageNMap<329		Key = (330			Key<Blake2_128Concat, CollectionId>,331			Key<Blake2_128Concat, T::CrossAccountId>,332		),333		Value = bool,334		QueryKind = ValueQuery,335	>;336337	/// Allowlisted collection users338	#[pallet::storage]339	pub type Allowlist<T: Config> = StorageNMap<340		Key = (341			Key<Blake2_128Concat, CollectionId>,342			Key<Blake2_128Concat, T::CrossAccountId>,343		),344		Value = bool,345		QueryKind = ValueQuery,346	>;347}348349impl<T: Config> Pallet<T> {350	/// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens351	pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {352		ensure!(353			&T::CrossAccountId::from_eth(H160([0; 20])) != receiver,354			<Error<T>>::AddressIsZero355		);356		Ok(())357	}358}359360impl<T: Config> Pallet<T> {361	pub fn init_collection(data: Collection<T>) -> Result<CollectionId, DispatchError> {362		{363			ensure!(364				data.name.len() <= MAX_COLLECTION_NAME_LENGTH,365				Error::<T>::CollectionNameLimitExceeded366			);367			ensure!(368				data.description.len() <= MAX_COLLECTION_DESCRIPTION_LENGTH,369				Error::<T>::CollectionDescriptionLimitExceeded370			);371			ensure!(372				data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH,373				Error::<T>::CollectionTokenPrefixLimitExceeded374			);375		}376377		let created_count = <CreatedCollectionCount<T>>::get()378			.0379			.checked_add(1)380			.ok_or(ArithmeticError::Overflow)?;381		let destroyed_count = <DestroyedCollectionCount<T>>::get().0;382		let id = CollectionId(created_count);383384		// bound Total number of collections385		ensure!(386			created_count - destroyed_count < COLLECTION_NUMBER_LIMIT,387			<Error<T>>::TotalCollectionsLimitExceeded388		);389390		// =========391392		// Take a (non-refundable) deposit of collection creation393		{394			let mut imbalance =395				<<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();396			imbalance.subsume(397				<<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(398					&T::TreasuryAccountId::get(),399					T::CollectionCreationPrice::get(),400				),401			);402			<T as Config>::Currency::settle(403				&data.owner,404				imbalance,405				WithdrawReasons::TRANSFER,406				ExistenceRequirement::KeepAlive,407			)408			.map_err(|_| Error::<T>::NoPermission)?;409		}410411		<CreatedCollectionCount<T>>::put(created_count);412		<Pallet<T>>::deposit_event(Event::CollectionCreated(413			id,414			data.mode.id(),415			data.owner.clone(),416		));417		<CollectionById<T>>::insert(id, data);418		Ok(id)419	}420421	pub fn destroy_collection(422		collection: CollectionHandle<T>,423		sender: &T::CrossAccountId,424	) -> DispatchResult {425		ensure!(426			collection.limits.owner_can_destroy(),427			<Error<T>>::NoPermission,428		);429		collection.check_is_owner(&sender)?;430431		let destroyed_collections = <DestroyedCollectionCount<T>>::get()432			.0433			.checked_add(1)434			.ok_or(ArithmeticError::Overflow)?;435436		// =========437438		<DestroyedCollectionCount<T>>::put(destroyed_collections);439		<CollectionById<T>>::remove(collection.id);440		<AdminAmount<T>>::remove(collection.id);441		<IsAdmin<T>>::remove_prefix((collection.id,), None);442		<Allowlist<T>>::remove_prefix((collection.id,), None);443		Ok(())444	}445446	pub fn toggle_allowlist(447		collection: &CollectionHandle<T>,448		sender: &T::CrossAccountId,449		user: &T::CrossAccountId,450		allowed: bool,451	) -> DispatchResult {452		collection.check_is_owner_or_admin(&sender)?;453454		// =========455456		if allowed {457			<Allowlist<T>>::insert((collection.id, user), true);458		} else {459			<Allowlist<T>>::remove((collection.id, user));460		}461462		Ok(())463	}464465	pub fn toggle_admin(466		collection: &CollectionHandle<T>,467		sender: &T::CrossAccountId,468		user: &T::CrossAccountId,469		admin: bool,470	) -> DispatchResult {471		collection.check_is_owner_or_admin(&sender)?;472473		let was_admin = <IsAdmin<T>>::get((collection.id, user));474		if was_admin == admin {475			return Ok(());476		}477		let amount = <AdminAmount<T>>::get(collection.id);478479		if admin {480			let amount = amount481				.checked_add(1)482				.ok_or(<Error<T>>::CollectionAdminAmountExceeded)?;483			ensure!(484				amount <= Self::collection_admins_limit(),485				<Error<T>>::CollectionAdminAmountExceeded,486			);487488			// =========489490			<AdminAmount<T>>::insert(collection.id, amount);491			<IsAdmin<T>>::insert((collection.id, user), true);492		} else {493			<AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));494			<IsAdmin<T>>::remove((collection.id, user));495		}496497		Ok(())498	}499}500501#[macro_export]502macro_rules! unsupported {503	() => {504		Err(<Error<T>>::UnsupportedOperation.into())505	};506}507508/// Worst cases509pub trait CommonWeightInfo {510	fn create_item() -> Weight;511	fn create_multiple_items(amount: u32) -> Weight;512	fn burn_item() -> Weight;513	fn transfer() -> Weight;514	fn approve() -> Weight;515	fn transfer_from() -> Weight;516	fn burn_from() -> Weight;517	fn set_variable_metadata(bytes: u32) -> Weight;518}519520pub trait CommonCollectionOperations<T: Config> {521	fn create_item(522		&self,523		sender: T::CrossAccountId,524		to: T::CrossAccountId,525		data: CreateItemData,526	) -> DispatchResultWithPostInfo;527	fn create_multiple_items(528		&self,529		sender: T::CrossAccountId,530		to: T::CrossAccountId,531		data: Vec<CreateItemData>,532	) -> DispatchResultWithPostInfo;533	fn burn_item(534		&self,535		sender: T::CrossAccountId,536		token: TokenId,537		amount: u128,538	) -> DispatchResultWithPostInfo;539540	fn transfer(541		&self,542		sender: T::CrossAccountId,543		to: T::CrossAccountId,544		token: TokenId,545		amount: u128,546	) -> DispatchResultWithPostInfo;547	fn approve(548		&self,549		sender: T::CrossAccountId,550		spender: T::CrossAccountId,551		token: TokenId,552		amount: u128,553	) -> DispatchResultWithPostInfo;554	fn transfer_from(555		&self,556		sender: T::CrossAccountId,557		from: T::CrossAccountId,558		to: T::CrossAccountId,559		token: TokenId,560		amount: u128,561	) -> DispatchResultWithPostInfo;562	fn burn_from(563		&self,564		sender: T::CrossAccountId,565		from: T::CrossAccountId,566		token: TokenId,567		amount: u128,568	) -> DispatchResultWithPostInfo;569570	fn set_variable_metadata(571		&self,572		sender: T::CrossAccountId,573		token: TokenId,574		data: Vec<u8>,575	) -> DispatchResultWithPostInfo;576577	fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;578	fn token_exists(&self, token: TokenId) -> bool;579	fn last_token_id(&self) -> TokenId;580581	fn token_owner(&self, token: TokenId) -> T::CrossAccountId;582	fn const_metadata(&self, token: TokenId) -> Vec<u8>;583	fn variable_metadata(&self, token: TokenId) -> Vec<u8>;584585	/// How many tokens collection contains (Applicable to nonfungible/refungible)586	fn collection_tokens(&self) -> u32;587	/// Amount of different tokens account has (Applicable to nonfungible/refungible)588	fn account_balance(&self, account: T::CrossAccountId) -> u32;589	/// Amount of specific token account have (Applicable to fungible/refungible)590	fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;591	fn allowance(592		&self,593		sender: T::CrossAccountId,594		spender: T::CrossAccountId,595		token: TokenId,596	) -> u128;597}598599// Flexible enough for implementing CommonCollectionOperations600pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {601	let post_info = PostDispatchInfo {602		actual_weight: Some(weight),603		pays_fee: Pays::Yes,604	};605	match res {606		Ok(()) => Ok(post_info),607		Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),608	}609}
modifiedpallets/nft/src/lib.rsdiffbeforeafterboth
--- a/pallets/nft/src/lib.rs
+++ b/pallets/nft/src/lib.rs
@@ -36,8 +36,8 @@
 use sp_runtime::{sp_std::prelude::Vec};
 use nft_data_structs::{
 	MAX_DECIMAL_POINTS, MAX_SPONSOR_TIMEOUT, MAX_TOKEN_OWNERSHIP, CUSTOM_DATA_LIMIT,
-	VARIABLE_ON_CHAIN_SCHEMA_LIMIT, CONST_ON_CHAIN_SCHEMA_LIMIT, COLLECTION_ADMINS_LIMIT,
-	OFFCHAIN_SCHEMA_LIMIT, FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,
+	VARIABLE_ON_CHAIN_SCHEMA_LIMIT, CONST_ON_CHAIN_SCHEMA_LIMIT, OFFCHAIN_SCHEMA_LIMIT,
+	FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,
 	NFT_SPONSOR_TRANSFER_TIMEOUT, AccessMode, Collection, CreateItemData, CollectionLimits,
 	CollectionId, CollectionMode, TokenId, SchemaVersion, SponsorshipState, MetaUpdatePermission,
 };
@@ -156,7 +156,6 @@
 	where
 		origin: T::Origin
 	{
-		const CollectionAdminsLimit: u64 = COLLECTION_ADMINS_LIMIT;
 		type Error = Error<T>;
 
 		fn on_initialize(_now: T::BlockNumber) -> Weight {
@@ -406,12 +405,9 @@
 		#[transactional]
 		pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::CrossAccountId) -> DispatchResult {
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
-
 			let collection = <CollectionHandle<T>>::try_get(collection_id)?;
-			collection.check_is_owner_or_admin(&sender)?;
 
-			<IsAdmin<T>>::insert((collection_id, new_admin_id.as_sub()), true);
-			Ok(())
+			<PalletCommon<T>>::toggle_admin(&collection, &sender, &new_admin_id, true)
 		}
 
 		/// Remove admin address of the Collection. An admin address can remove itself. List of admins may become empty, in which case only Collection Owner will be able to add an Admin.
@@ -430,12 +426,9 @@
 		#[transactional]
 		pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::CrossAccountId) -> DispatchResult {
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
-
 			let collection = <CollectionHandle<T>>::try_get(collection_id)?;
-			collection.check_is_owner_or_admin(&sender)?;
 
-			<IsAdmin<T>>::remove((collection_id, account_id.as_sub()));
-			Ok(())
+			<PalletCommon<T>>::toggle_admin(&collection, &sender, &account_id, false)
 		}
 
 		/// # Permissions
modifiedprimitives/nft/src/lib.rsdiffbeforeafterboth
--- a/primitives/nft/src/lib.rs
+++ b/primitives/nft/src/lib.rs
@@ -41,7 +41,7 @@
 } else {
 	10
 };
-pub const COLLECTION_ADMINS_LIMIT: u64 = 5;
+pub const COLLECTION_ADMINS_LIMIT: u32 = 5;
 pub const COLLECTION_TOKEN_LIMIT: u32 = u32::MAX;
 pub const ACCOUNT_TOKEN_OWNERSHIP_LIMIT: u32 = if cfg!(not(feature = "limit-testing")) {
 	1000000