--- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -171,7 +171,8 @@ sponsor: get_account_id_from_seed::("Alice"), unconfirmed_sponsor: get_account_id_from_seed::("Alice"), const_on_chain_schema: vec![], - variable_on_chain_schema: vec![] + variable_on_chain_schema: vec![], + limits: CollectionLimits::default() }, )], nft_item_id: vec![], --- a/pallets/nft/src/lib.rs +++ b/pallets/nft/src/lib.rs @@ -46,6 +46,8 @@ mod default_weights; pub const MAX_DECIMAL_POINTS: DecimalPoints = 30; +pub const MAX_SPONSOR_TIMEOUT: u32 = 10_368_000; +pub const MAX_TOKEN_OWNERSHIP: u32 = 10_000_000; // Structs // #region @@ -116,6 +118,7 @@ pub offchain_schema: Vec, pub sponsor: AccountId, // Who pays fees. If set to default address, the fees are applied to the transaction sender pub unconfirmed_sponsor: AccountId, // Sponsor address that has not yet confirmed sponsorship + pub limits: CollectionLimits, // Collection private restrictions pub variable_on_chain_schema: Vec, // pub const_on_chain_schema: Vec, // } @@ -171,6 +174,27 @@ pub start_block: BlockNumber, } +#[derive(Encode, Decode, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct CollectionLimits { + pub account_token_ownership_limit: u32, + pub sponsored_data_size: u32, + pub token_limit: u32, + + // Timeouts for item types in passed blocks + pub sponsor_transfer_timeout: u32, +} + +impl Default for CollectionLimits { + fn default() -> CollectionLimits { + CollectionLimits { + account_token_ownership_limit: 10_000_000, + token_limit: u32::max_value(), + sponsored_data_size: u32::max_value(), + sponsor_transfer_timeout: 14400 } + } +} + #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct ChainLimits { @@ -328,7 +352,13 @@ /// Unexpected collection type. UnexpectedCollectionType, /// Can't store metadata in fungible tokens. - CantStoreMetadataInFungibleTokens + CantStoreMetadataInFungibleTokens, + /// Collection token limit exceeded + CollectionTokenLimitExceeded, + /// Account token limit exceeded per collection + AccountTokenLimitExceeded, + /// Collection limit bounds per collection exceeded + CollectionLimitBoundsExceeded } } @@ -544,6 +574,7 @@ unconfirmed_sponsor: T::AccountId::default(), variable_on_chain_schema: Vec::new(), const_on_chain_schema: Vec::new(), + limits: CollectionLimits::default(), }; // Add new collection to map @@ -1022,9 +1053,12 @@ pub fn transfer(origin, recipient: T::AccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResult { let sender = ensure_signed(origin)?; + let target_collection = >::get(collection_id); + + // Limits check + Self::is_correct_transfer(collection_id, &target_collection, &recipient)?; // Transfer permissions check - let target_collection = >::get(collection_id); ensure!(Self::is_item_owner(sender.clone(), collection_id, item_id) || Self::is_owner_or_admin_permissions(collection_id, sender.clone()), Error::::NoPermission); @@ -1135,11 +1169,15 @@ } } - // Transfer permissions check let target_collection = >::get(collection_id); - ensure!(appoved_transfer || Self::is_owner_or_admin_permissions(collection_id, sender.clone()), - Error::::NoPermission); + // Limits check + Self::is_correct_transfer(collection_id, &target_collection, &recipient)?; + + // Transfer permissions check + ensure!(appoved_transfer || Self::is_owner_or_admin_permissions(collection_id, sender.clone()), + Error::::NoPermission); + if target_collection.access == AccessMode::WhiteList { Self::check_white_list(collection_id, &sender)?; Self::check_white_list(collection_id, &recipient)?; @@ -1387,24 +1425,55 @@ Ok(()) } - // #[cfg(feature = "runtime-benchmarks")] - // #[weight = 0] - // pub fn add_contract_sponsoring_debug( - // origin, - // contract_address: T::AccountId, - // owner: T::AccountId) -> DispatchResult { - // let sender = ensure_signed(origin)?; - // >::insert(contract_address.clone(), owner); - // Ok(()) - // } - + #[weight = 0] + pub fn set_collection_limits( + origin, + collection_id: u32, + limits: CollectionLimits, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::check_owner_permissions(collection_id, sender.clone())?; + let mut target_collection = >::get(collection_id); + let chain_limits = ChainLimit::get(); + let climits = target_collection.limits; + + // collection bounds + ensure!(limits.sponsor_transfer_timeout <= MAX_SPONSOR_TIMEOUT && + limits.account_token_ownership_limit <= MAX_TOKEN_OWNERSHIP, + Error::::CollectionLimitBoundsExceeded); + + // token_limit check prev + ensure!(climits.token_limit > limits.token_limit && + limits.token_limit <= chain_limits.account_token_ownership_limit, + Error::::AccountTokenLimitExceeded); + + target_collection.limits = limits; + >::insert(collection_id, target_collection); + + Ok(()) + } } } impl Module { + fn is_correct_transfer(collection_id: CollectionId, collection: &CollectionType, recipient: &T::AccountId) -> DispatchResult { + + // check token limit and account token limit + let account_items: u32 = >::get(collection_id, recipient).len() as u32; + ensure!(collection.limits.account_token_ownership_limit > account_items, Error::::AccountTokenLimitExceeded); + + Ok(()) + } + fn can_create_items_in_collection(collection_id: CollectionId, collection: &CollectionType, sender: &T::AccountId, owner: &T::AccountId) -> DispatchResult { + // check token limit and account token limit + let total_items: u32 = ItemListIndex::get(collection_id); + let account_items: u32 = >::get(collection_id, owner).len() as u32; + ensure!(collection.limits.token_limit > total_items, Error::::CollectionTokenLimitExceeded); + ensure!(collection.limits.account_token_ownership_limit > account_items, Error::::AccountTokenLimitExceeded); + if !Self::is_owner_or_admin_permissions(collection_id, sender.clone()) { ensure!(collection.mint_mode == true, Error::::PublicMintingNotAllowed); Self::check_white_list(collection_id, owner)?; @@ -1485,7 +1554,6 @@ Self::add_refungible_item(item)?; } }; - // call event Self::deposit_event(RawEvent::ItemCreated(collection_id, ::get(collection_id))); @@ -2213,18 +2281,35 @@ // Determine who is paying transaction fee based on ecnomic model // Parse call to extract collection ID and access collection sponsor let mut sponsor: T::AccountId = match IsSubType::>::is_sub_type(call) { - Some(Call::create_item(collection_id, _properties, _owner)) => { - >::get(collection_id).sponsor + Some(Call::create_item(collection_id, _owner, _properties)) => { + + // check free create limit + if >::get(collection_id).limits.sponsored_data_size >= (_properties.len() as u32) + { + >::get(collection_id).sponsor + } else { + T::AccountId::default() + } } Some(Call::transfer(_new_owner, collection_id, _item_id, _value)) => { + + let _collection_limits = >::get(collection_id).limits; let _collection_mode = >::get(collection_id).mode; // sponsor timeout let sponsor_transfer = match _collection_mode { CollectionMode::NFT => { + + // get correct limit + let limit: u32 = if _collection_limits.sponsor_transfer_timeout > 0 { + _collection_limits.sponsor_transfer_timeout + } else { + ChainLimit::get().nft_sponsor_transfer_timeout + }; + let basket = >::get(collection_id, _item_id); let block_number = >::block_number() as T::BlockNumber; - let limit_time = basket + ChainLimit::get().nft_sponsor_transfer_timeout.into(); + let limit_time = basket + limit.into(); if block_number >= limit_time { >::insert(collection_id, _item_id, block_number); true @@ -2234,12 +2319,20 @@ } } CollectionMode::Fungible(_) => { + + // get correct limit + let limit: u32 = if _collection_limits.sponsor_transfer_timeout > 0 { + _collection_limits.sponsor_transfer_timeout + } else { + ChainLimit::get().fungible_sponsor_transfer_timeout + }; + let mut basket = >::get(collection_id, _item_id); let block_number = >::block_number() as T::BlockNumber; if basket.iter().any(|i| i.address == _new_owner.clone()) { let item = basket.iter_mut().find(|i| i.address == _new_owner.clone()).unwrap().clone(); - let limit_time = item.start_block + ChainLimit::get().fungible_sponsor_transfer_timeout.into(); + let limit_time = item.start_block + limit.into(); if block_number >= limit_time { basket.retain(|x| x.address == item.address); basket.push(BasketItem { start_block: block_number, address: _new_owner.clone() }); @@ -2256,9 +2349,17 @@ } } CollectionMode::ReFungible(_) => { + + // get correct limit + let limit: u32 = if _collection_limits.sponsor_transfer_timeout > 0 { + _collection_limits.sponsor_transfer_timeout + } else { + ChainLimit::get().refungible_sponsor_transfer_timeout + }; + let basket = >::get(collection_id, _item_id); let block_number = >::block_number() as T::BlockNumber; - let limit_time = basket + ChainLimit::get().nft_sponsor_transfer_timeout.into(); + let limit_time = basket + limit.into(); if block_number >= limit_time { >::insert(collection_id, _item_id, block_number); true --- a/pallets/nft/src/tests.rs +++ b/pallets/nft/src/tests.rs @@ -3,7 +3,7 @@ use crate::mock::*; use crate::{AccessMode, ApprovePermissions, CollectionMode, Ownership, ChainLimits, CreateItemData, CreateNftData, CreateFungibleData, CreateReFungibleData, - CollectionId, TokenId, MAX_DECIMAL_POINTS}; //Err + CollectionId, TokenId, MAX_DECIMAL_POINTS}; use frame_support::{assert_noop, assert_ok}; use frame_system::{ RawOrigin }; --- a/runtime_types.json +++ b/runtime_types.json @@ -46,21 +46,12 @@ "Owner": "AccountId", "Value": "u128" }, - "ReFungibleItemType": { - "Collection": "CollectionId", - "Owner": "Vec", - "Data": "Vec" - }, "NftItemType": { "Collection": "CollectionId", "Owner": "AccountId", "ConstData": "Vec", "VariableData": "Vec" }, - "Ownership": { - "owner": "AccountId", - "fraction": "u128" - }, "ReFungibleItemType": { "Collection": "CollectionId", "Owner": "Vec>", @@ -79,6 +70,7 @@ "OffchainSchema": "Vec", "Sponsor": "AccountId", "UnconfirmedSponsor": "AccountId", + "Limits": "CollectionLimits", "VariableOnChainSchema": "Vec", "ConstOnChainSchema": "Vec" }, @@ -117,9 +109,15 @@ "account_token_ownership_limit": "u32", "collections_admins_limit": "u64", "custom_data_limit": "u32", - "nft_sponsor_transfer_timeout": "u32", - "fungible_sponsor_transfer_timeout": "u32", - "refungible_sponsor_transfer_timeout": "u32" + "nft_sponsor_timeout": "u32", + "fungible_sponsor_timeout": "u32", + "refungible_sponsor_timeout": "u32" + }, + "CollectionLimits": { + "AccountTokenOwnershipLimit": "u32", + "SponsoredMintSize": "u32", + "TokenLimit": "u32", + "SponsorTimeout": "u32" } } \ No newline at end of file