git.delta.rocks / unique-network / refs/commits / 836070f4956b

difftreelog

Limits logic fixed. Tests added

str-mv2021-09-08parent: #abfdac1.patch.diff
in: master

4 files changed

modifiedpallets/nft/src/lib.rsdiffbeforeafterboth
--- a/pallets/nft/src/lib.rs
+++ b/pallets/nft/src/lib.rs
@@ -302,9 +302,6 @@
 		/// Amount of collections destroyed, used for total amount tracking with
 		/// CreatedCollectionCount
 		DestroyedCollectionCount: u32;
-		/// Total amount of account owned tokens (NFTs + RFTs + unique fungibles)
-		/// Account id (real)
-		pub AccountItemCount get(fn account_item_count): map hasher(twox_64_concat) T::AccountId => u32;
 		//#endregion
 
 		//#region Basic collections
@@ -1595,11 +1592,19 @@
 		collection.consume_sload()?;
 		let account_items: u32 =
 			<AddressTokens<T>>::get(collection_id, recipient.as_sub()).len() as u32;
-		ensure!(
-			collection.limits.account_token_ownership_limit > account_items,
-			Error::<T>::AccountTokenLimitExceeded
-		);
 
+		// zero limit means collection limit is disabled
+		// otherwise get lower value
+		let limit = if collection.limits.account_token_ownership_limit == 0
+			|| collection.limits.account_token_ownership_limit > ACCOUNT_TOKEN_OWNERSHIP_LIMIT
+		{
+			ACCOUNT_TOKEN_OWNERSHIP_LIMIT
+		} else {
+			collection.limits.account_token_ownership_limit
+		};
+
+		ensure!(limit > account_items, Error::<T>::AccountTokenLimitExceeded);
+
 		// preliminary transfer check
 		ensure!(collection.transfers_enabled, Error::<T>::TransferNotAllowed);
 
@@ -1622,12 +1627,23 @@
 			as u32)
 			.checked_add(amount)
 			.ok_or(Error::<T>::AccountTokenLimitExceeded)?;
+
+		// zero limit means collection limit is disabled
+		// otherwise get lower value
+		let account_token_limit = if collection.limits.account_token_ownership_limit == 0
+			|| collection.limits.account_token_ownership_limit > ACCOUNT_TOKEN_OWNERSHIP_LIMIT
+		{
+			ACCOUNT_TOKEN_OWNERSHIP_LIMIT
+		} else {
+			collection.limits.account_token_ownership_limit
+		};
+
 		ensure!(
 			collection.limits.token_limit >= total_items,
 			Error::<T>::CollectionTokenLimitExceeded
 		);
 		ensure!(
-			collection.limits.account_token_ownership_limit >= account_items,
+			account_token_limit >= account_items,
 			Error::<T>::AccountTokenLimitExceeded
 		);
 
@@ -2362,32 +2378,19 @@
 		item_index: TokenId,
 		owner: &T::CrossAccountId,
 	) -> DispatchResult {
-		// add to account limit
 		collection.consume_sload()?;
-		if <AccountItemCount<T>>::contains_key(owner.as_sub()) {
-			// bound Owned tokens by a single address
+		let list_exists = <AddressTokens<T>>::contains_key(collection.id, owner.as_sub());
+		if list_exists {
 			collection.consume_sload()?;
-			let count = <AccountItemCount<T>>::get(owner.as_sub());
+			let mut list = <AddressTokens<T>>::get(collection.id, owner.as_sub());
+
+			// bound Owned tokens by a single address in collection
+			let account_items: u32 = list.len() as u32;
 			ensure!(
-				count < ACCOUNT_TOKEN_OWNERSHIP_LIMIT,
+				account_items < ACCOUNT_TOKEN_OWNERSHIP_LIMIT,
 				Error::<T>::AddressOwnershipLimitExceeded
 			);
-
-			collection.consume_sstore()?;
-			<AccountItemCount<T>>::insert(
-				owner.as_sub(),
-				count.checked_add(1).ok_or(Error::<T>::NumOverflow)?,
-			);
-		} else {
-			collection.consume_sstore()?;
-			<AccountItemCount<T>>::insert(owner.as_sub(), 1);
-		}
 
-		collection.consume_sload()?;
-		let list_exists = <AddressTokens<T>>::contains_key(collection.id, owner.as_sub());
-		if list_exists {
-			collection.consume_sload()?;
-			let mut list = <AddressTokens<T>>::get(collection.id, owner.as_sub());
 			let item_contains = list.contains(&item_index.clone());
 
 			if !item_contains {
@@ -2410,16 +2413,6 @@
 		item_index: TokenId,
 		owner: &T::CrossAccountId,
 	) -> DispatchResult {
-		// update counter
-		collection.consume_sload()?;
-		collection.consume_sstore()?;
-		<AccountItemCount<T>>::insert(
-			owner.as_sub(),
-			<AccountItemCount<T>>::get(owner.as_sub())
-				.checked_sub(1)
-				.ok_or(Error::<T>::NumOverflow)?,
-		);
-
 		collection.consume_sload()?;
 		let list_exists = <AddressTokens<T>>::contains_key(collection.id, owner.as_sub());
 		if list_exists {
modifiedpallets/nft/src/sponsorship.rsdiffbeforeafterboth
before · pallets/nft/src/sponsorship.rs
1use crate::{2	Config, Call, CollectionById, CreateItemBasket, VariableMetaDataBasket,3	ReFungibleTransferBasket, FungibleTransferBasket, NftTransferBasket, CreateItemData,4	CollectionMode,5};6use core::marker::PhantomData;7use up_sponsorship::SponsorshipHandler;8use frame_support::{9	traits::{IsSubType},10	storage::{StorageMap, StorageDoubleMap},11};12use nft_data_structs::{13	TokenId, CollectionId, NFT_SPONSOR_TRANSFER_TIMEOUT, REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,14	FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,15};1617pub struct NftSponsorshipHandler<T>(PhantomData<T>);18impl<T: Config> NftSponsorshipHandler<T> {19	pub fn withdraw_create_item(20		who: &T::AccountId,21		collection_id: &CollectionId,22		_properties: &CreateItemData,23	) -> Option<T::AccountId> {24		let collection = CollectionById::<T>::get(collection_id)?;2526		// sponsor timeout27		let block_number = <frame_system::Pallet<T>>::block_number() as T::BlockNumber;2829		let limit = collection.limits.sponsor_transfer_timeout;30		if CreateItemBasket::<T>::contains_key((collection_id, &who)) {31			let last_tx_block = CreateItemBasket::<T>::get((collection_id, &who));32			let limit_time = last_tx_block + limit.into();33			if block_number <= limit_time {34				return None;35			}36		}37		CreateItemBasket::<T>::insert((collection_id, who.clone()), block_number);3839		// check free create limit40		if collection.limits.sponsored_data_size >= (_properties.data_size() as u32) {41			collection.sponsorship.sponsor().cloned()42		} else {43			None44		}45	}4647	pub fn withdraw_transfer(48		who: &T::AccountId,49		collection_id: &CollectionId,50		item_id: &TokenId,51	) -> Option<T::AccountId> {52		let collection = CollectionById::<T>::get(collection_id)?;5354		let mut sponsor_transfer = false;55		if collection.sponsorship.confirmed() {56			let collection_limits = collection.limits.clone();57			let collection_mode = collection.mode.clone();5859			// sponsor timeout60			let block_number = <frame_system::Pallet<T>>::block_number() as T::BlockNumber;61			sponsor_transfer = match collection_mode {62				CollectionMode::NFT => {63					// get correct limit64					let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 {65						collection_limits.sponsor_transfer_timeout66					} else {67						NFT_SPONSOR_TRANSFER_TIMEOUT68					};6970					let mut sponsored = true;71					if NftTransferBasket::<T>::contains_key(collection_id, item_id) {72						let last_tx_block = NftTransferBasket::<T>::get(collection_id, item_id);73						let limit_time = last_tx_block + limit.into();74						if block_number <= limit_time {75							sponsored = false;76						}77					}78					if sponsored {79						NftTransferBasket::<T>::insert(collection_id, item_id, block_number);80					}8182					sponsored83				}84				CollectionMode::Fungible(_) => {85					// get correct limit86					let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 {87						collection_limits.sponsor_transfer_timeout88					} else {89						FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT90					};9192					let block_number = <frame_system::Pallet<T>>::block_number() as T::BlockNumber;93					let mut sponsored = true;94					if FungibleTransferBasket::<T>::contains_key(collection_id, who) {95						let last_tx_block = FungibleTransferBasket::<T>::get(collection_id, who);96						let limit_time = last_tx_block + limit.into();97						if block_number <= limit_time {98							sponsored = false;99						}100					}101					if sponsored {102						FungibleTransferBasket::<T>::insert(collection_id, who, block_number);103					}104105					sponsored106				}107				CollectionMode::ReFungible => {108					// get correct limit109					let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 {110						collection_limits.sponsor_transfer_timeout111					} else {112						REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT113					};114115					let mut sponsored = true;116					if ReFungibleTransferBasket::<T>::contains_key(collection_id, item_id) {117						let last_tx_block =118							ReFungibleTransferBasket::<T>::get(collection_id, item_id);119						let limit_time = last_tx_block + limit.into();120						if block_number <= limit_time {121							sponsored = false;122						}123					}124					if sponsored {125						ReFungibleTransferBasket::<T>::insert(collection_id, item_id, block_number);126					}127128					sponsored129				}130				_ => false,131			};132		}133134		if !sponsor_transfer {135			None136		} else {137			collection.sponsorship.sponsor().cloned()138		}139	}140141	pub fn withdraw_set_variable_meta_data(142		collection_id: &CollectionId,143		item_id: &TokenId,144		data: &[u8],145	) -> Option<T::AccountId> {146		let mut sponsor_metadata_changes = false;147148		let collection = CollectionById::<T>::get(collection_id)?;149150		if collection.sponsorship.confirmed() &&151			// Can't sponsor fungible collection, this tx will be rejected152			// as invalid153			!matches!(collection.mode, CollectionMode::Fungible(_)) &&154			data.len() <= collection.limits.sponsored_data_size as usize155		{156			if let Some(rate_limit) = collection.limits.sponsored_data_rate_limit {157				let block_number = <frame_system::Pallet<T>>::block_number() as T::BlockNumber;158159				if VariableMetaDataBasket::<T>::get(collection_id, item_id)160					.map(|last_block| block_number - last_block > rate_limit)161					.unwrap_or(true)162				{163					sponsor_metadata_changes = true;164					VariableMetaDataBasket::<T>::insert(collection_id, item_id, block_number);165				}166			}167		}168169		if !sponsor_metadata_changes {170			None171		} else {172			collection.sponsorship.sponsor().cloned()173		}174	}175}176177impl<T, C> SponsorshipHandler<T::AccountId, C> for NftSponsorshipHandler<T>178where179	T: Config,180	C: IsSubType<Call<T>>,181{182	fn get_sponsor(who: &T::AccountId, call: &C) -> Option<T::AccountId> {183		match IsSubType::<Call<T>>::is_sub_type(call)? {184			Call::create_item(collection_id, _owner, properties) => {185				Self::withdraw_create_item(who, collection_id, properties)186			}187			Call::transfer(_new_owner, collection_id, item_id, _value) => {188				Self::withdraw_transfer(who, collection_id, item_id)189			}190			Call::set_variable_meta_data(collection_id, item_id, data) => {191				Self::withdraw_set_variable_meta_data(collection_id, item_id, data)192			}193			_ => None,194		}195	}196}
addedtests/src/limits.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/limits.test.ts
@@ -0,0 +1,378 @@
+//
+// This file is subject to the terms and conditions defined in
+// file 'LICENSE', which is part of this source code package.
+//
+
+import { IKeyringPair } from '@polkadot/types/types';
+import privateKey from './substrate/privateKey';
+import usingApi from './substrate/substrate-api';
+import {
+  createCollectionExpectSuccess,
+  destroyCollectionExpectSuccess,
+  setCollectionLimitsExpectSuccess,
+  setCollectionSponsorExpectSuccess,
+  confirmSponsorshipExpectSuccess,
+  createItemExpectSuccess,
+  createItemExpectFailure,
+  transferExpectSuccess,
+  getFreeBalance,
+  waitNewBlocks,
+} from './util/helpers'; 
+import { expect } from 'chai';
+
+describe('Number of tokens per address (NFT)', () => {
+  let Alice: IKeyringPair;
+
+  before(async () => {
+    await usingApi(async () => {
+      Alice = privateKey('//Alice');
+    });
+  });
+
+  it('Collection limits allow greater number than chain limits, chain limits are enforced', async () => {
+      
+    const collectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } });
+    await setCollectionLimitsExpectSuccess(Alice, collectionId, { AccountTokenOwnershipLimit: 20 });
+    for(let i = 0; i < 10; i++){
+      await createItemExpectSuccess(Alice, collectionId, 'NFT');
+    }
+    await createItemExpectFailure(Alice, collectionId, 'NFT');
+    await destroyCollectionExpectSuccess(collectionId);
+  });
+
+  it('Collection limits allow lower number than chain limits, collection limits are enforced', async () => {
+
+    const collectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } });
+    await setCollectionLimitsExpectSuccess(Alice, collectionId, { AccountTokenOwnershipLimit: 1 });
+    await createItemExpectSuccess(Alice, collectionId, 'NFT');
+    await createItemExpectFailure(Alice, collectionId, 'NFT');
+    await destroyCollectionExpectSuccess(collectionId);
+  });
+});
+
+describe('Number of tokens per address (ReFungible)', () => {
+  let Alice: IKeyringPair;
+
+  before(async () => {
+    await usingApi(async () => {
+      Alice = privateKey('//Alice');
+    });
+  });
+
+  it('Collection limits allow greater number than chain limits, chain limits are enforced', async () => {   
+    const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible' }});
+    await setCollectionLimitsExpectSuccess(Alice, collectionId, { AccountTokenOwnershipLimit: 20 });
+    for(let i = 0; i < 10; i++){
+      await createItemExpectSuccess(Alice, collectionId, 'ReFungible');
+    }
+    await createItemExpectFailure(Alice, collectionId, 'ReFungible');
+    await destroyCollectionExpectSuccess(collectionId);
+  });
+
+  it('Collection limits allow lower number than chain limits, collection limits are enforced', async () => {
+    const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible' }});
+    await setCollectionLimitsExpectSuccess(Alice, collectionId, { AccountTokenOwnershipLimit: 1 });
+    await createItemExpectSuccess(Alice, collectionId, 'ReFungible');
+    await createItemExpectFailure(Alice, collectionId, 'ReFungible');
+    await destroyCollectionExpectSuccess(collectionId);
+  });
+});
+
+describe('Sponsor timeout (NFT)', () => {
+  let Alice: IKeyringPair;
+  let Bob: IKeyringPair;
+  let Charlie: IKeyringPair;
+
+  before(async () => {
+    await usingApi(async () => {
+      Alice = privateKey('//Alice');
+      Bob = privateKey('//Bob');
+      Charlie = privateKey('//Charlie');
+    });
+  });
+
+  it('Collection limits have greater timeout value than chain limits, collection limits are enforced', async () => {  
+    const collectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } });
+    await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 7 });
+    const tokenId = await createItemExpectSuccess(Alice, collectionId, 'NFT');
+    await setCollectionSponsorExpectSuccess(collectionId, Alice.address);
+    await confirmSponsorshipExpectSuccess(collectionId, '//Alice');
+    await transferExpectSuccess(collectionId, tokenId, Alice, Bob);
+    const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber();
+
+    // check setting SponsorTimeout = 5, fail
+    await waitNewBlocks(5);
+    await transferExpectSuccess(collectionId, tokenId, Bob, Charlie);
+    const aliceBalanceAfterUnsponsoredTransaction = (await getFreeBalance(Alice)).toNumber();
+    expect(aliceBalanceAfterUnsponsoredTransaction).to.be.equals(aliceBalanceBefore);
+
+    // check setting SponsorTimeout = 7, success
+    await waitNewBlocks(2); // 5 + 2
+    await transferExpectSuccess(collectionId, tokenId, Charlie, Bob);
+    const aliceBalanceAfterSponsoredTransaction = (await getFreeBalance(Alice)).toNumber();
+    expect(aliceBalanceAfterSponsoredTransaction).to.be.lessThan(aliceBalanceBefore);
+    await destroyCollectionExpectSuccess(collectionId);
+  });
+
+  it('Collection limits have lower timeout value than chain limits, chain limits are enforced', async () => {
+
+    const collectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } });
+    await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 1 });
+    const tokenId = await createItemExpectSuccess(Alice, collectionId, 'NFT');
+    await setCollectionSponsorExpectSuccess(collectionId, Alice.address);
+    await confirmSponsorshipExpectSuccess(collectionId, '//Alice');
+    await transferExpectSuccess(collectionId, tokenId, Alice, Bob);
+    const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber();
+
+    // check setting SponsorTimeout = 1, fail
+    await waitNewBlocks(1);
+    await transferExpectSuccess(collectionId, tokenId, Bob, Charlie);
+    const aliceBalanceAfterUnsponsoredTransaction = (await getFreeBalance(Alice)).toNumber();
+    expect(aliceBalanceAfterUnsponsoredTransaction).to.be.equals(aliceBalanceBefore);
+
+    // check setting SponsorTimeout = 5, success
+    await waitNewBlocks(4);
+    await transferExpectSuccess(collectionId, tokenId, Charlie, Bob);
+    const aliceBalanceAfterSponsoredTransaction = (await getFreeBalance(Alice)).toNumber();
+    expect(aliceBalanceAfterSponsoredTransaction).to.be.lessThan(aliceBalanceBefore);
+    await destroyCollectionExpectSuccess(collectionId);
+  });
+});
+
+describe('Sponsor timeout (Fungible)', () => {
+  let Alice: IKeyringPair;
+  let Bob: IKeyringPair;
+  let Charlie: IKeyringPair;
+
+  before(async () => {
+    await usingApi(async () => {
+      Alice = privateKey('//Alice');
+      Bob = privateKey('//Bob');
+      Charlie = privateKey('//Charlie');
+    });
+  });
+
+  it('Collection limits have greater timeout value than chain limits, collection limits are enforced', async () => {  
+    const collectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
+    await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 7 });
+    const tokenId = await createItemExpectSuccess(Alice, collectionId, 'Fungible');
+    await setCollectionSponsorExpectSuccess(collectionId, Alice.address);
+    await confirmSponsorshipExpectSuccess(collectionId, '//Alice');
+    await transferExpectSuccess(collectionId, tokenId, Alice, Bob, 10, 'Fungible');
+    await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 2, 'Fungible');
+    const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber();
+
+    // check setting SponsorTimeout = 5, fail
+    await waitNewBlocks(5);
+    await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 2, 'Fungible');
+    const aliceBalanceAfterUnsponsoredTransaction = (await getFreeBalance(Alice)).toNumber();
+    expect(aliceBalanceAfterUnsponsoredTransaction).to.be.equals(aliceBalanceBefore);
+
+    // check setting SponsorTimeout = 7, success
+    await waitNewBlocks(2); // 5 + 2
+    await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 2, 'Fungible');
+    const aliceBalanceAfterSponsoredTransaction = (await getFreeBalance(Alice)).toNumber();
+    expect(aliceBalanceAfterSponsoredTransaction).to.be.lessThan(aliceBalanceBefore);
+
+    await destroyCollectionExpectSuccess(collectionId);
+  });
+
+  it('Collection limits have lower timeout value than chain limits, chain limits are enforced', async () => {
+
+    const collectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
+    await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 1 });
+    const tokenId = await createItemExpectSuccess(Alice, collectionId, 'Fungible');
+    await setCollectionSponsorExpectSuccess(collectionId, Alice.address);
+    await confirmSponsorshipExpectSuccess(collectionId, '//Alice');
+    await transferExpectSuccess(collectionId, tokenId, Alice, Bob, 10, 'Fungible');
+    await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 2, 'Fungible');
+    const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber();
+
+    // check setting SponsorTimeout = 1, fail
+    await waitNewBlocks(1);
+    await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 2, 'Fungible');
+    const aliceBalanceAfterUnsponsoredTransaction = (await getFreeBalance(Alice)).toNumber();
+    expect(aliceBalanceAfterUnsponsoredTransaction).to.be.equals(aliceBalanceBefore);
+
+    // check setting SponsorTimeout = 5, success
+    await waitNewBlocks(4);
+    await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 2, 'Fungible');
+    const aliceBalanceAfterSponsoredTransaction = (await getFreeBalance(Alice)).toNumber();
+    expect(aliceBalanceAfterSponsoredTransaction).to.be.lessThan(aliceBalanceBefore);
+
+    await destroyCollectionExpectSuccess(collectionId);
+  });
+});
+
+describe('Sponsor timeout (ReFungible)', () => {
+  let Alice: IKeyringPair;
+  let Bob: IKeyringPair;
+  let Charlie: IKeyringPair;
+
+  before(async () => {
+    await usingApi(async () => {
+      Alice = privateKey('//Alice');
+      Bob = privateKey('//Bob');
+      Charlie = privateKey('//Charlie');
+    });
+  });
+
+  it('Collection limits have greater timeout value than chain limits, collection limits are enforced', async () => {  
+    const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible' }});
+    await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 7 });
+    const tokenId = await createItemExpectSuccess(Alice, collectionId, 'ReFungible');
+    await setCollectionSponsorExpectSuccess(collectionId, Alice.address);
+    await confirmSponsorshipExpectSuccess(collectionId, '//Alice');
+    await transferExpectSuccess(collectionId, tokenId, Alice, Bob, 100, 'ReFungible');
+    const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber();
+
+    // check setting SponsorTimeout = 5, fail
+    await waitNewBlocks(5);
+    await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 20, 'ReFungible');
+    const aliceBalanceAfterUnsponsoredTransaction = (await getFreeBalance(Alice)).toNumber();
+    expect(aliceBalanceAfterUnsponsoredTransaction).to.be.equals(aliceBalanceBefore);
+
+    // check setting SponsorTimeout = 7, success
+    await waitNewBlocks(2); // 5 + 2
+    await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 20, 'ReFungible');
+    const aliceBalanceAfterSponsoredTransaction = (await getFreeBalance(Alice)).toNumber();
+    expect(aliceBalanceAfterSponsoredTransaction).to.be.lessThan(aliceBalanceBefore);
+    await destroyCollectionExpectSuccess(collectionId);
+  });
+
+  it('Collection limits have lower timeout value than chain limits, chain limits are enforced', async () => {
+
+    const collectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } });
+    await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 1 });
+    const tokenId = await createItemExpectSuccess(Alice, collectionId, 'NFT');
+    await setCollectionSponsorExpectSuccess(collectionId, Alice.address);
+    await confirmSponsorshipExpectSuccess(collectionId, '//Alice');
+    await transferExpectSuccess(collectionId, tokenId, Alice, Bob);
+    const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber();
+
+    // check setting SponsorTimeout = 1, fail
+    await waitNewBlocks(1);
+    await transferExpectSuccess(collectionId, tokenId, Bob, Charlie);
+    const aliceBalanceAfterUnsponsoredTransaction = (await getFreeBalance(Alice)).toNumber();
+    expect(aliceBalanceAfterUnsponsoredTransaction).to.be.equals(aliceBalanceBefore);
+
+    // check setting SponsorTimeout = 5, success
+    await waitNewBlocks(4);
+    await transferExpectSuccess(collectionId, tokenId, Charlie, Bob);
+    const aliceBalanceAfterSponsoredTransaction = (await getFreeBalance(Alice)).toNumber();
+    expect(aliceBalanceAfterSponsoredTransaction).to.be.lessThan(aliceBalanceBefore);
+    await destroyCollectionExpectSuccess(collectionId);
+  });
+});
+
+describe('Collection zero limits (NFT)', () => {
+  let Alice: IKeyringPair;
+  let Bob: IKeyringPair;
+  let Charlie: IKeyringPair;
+
+  before(async () => {
+    await usingApi(async () => {
+      Alice = privateKey('//Alice');
+      Bob = privateKey('//Bob');
+      Charlie = privateKey('//Charlie');
+    });
+  });
+
+  it('Limits have 0 in tokens per address field, the chain limits are applied', async () => {  
+    const collectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } });
+    await setCollectionLimitsExpectSuccess(Alice, collectionId, { AccountTokenOwnershipLimit: 0 });
+    for(let i = 0; i < 10; i++){
+      await createItemExpectSuccess(Alice, collectionId, 'NFT');
+    }
+    await createItemExpectFailure(Alice, collectionId, 'NFT');
+  });
+
+  it('Limits have 0 in sponsor timeout, no limits are applied', async () => {
+
+    const collectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } });
+    await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 0 });
+    const tokenId = await createItemExpectSuccess(Alice, collectionId, 'NFT');
+    await setCollectionSponsorExpectSuccess(collectionId, Alice.address);
+    await confirmSponsorshipExpectSuccess(collectionId, '//Alice');
+    await transferExpectSuccess(collectionId, tokenId, Alice, Bob);
+    const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber();
+
+    // check setting SponsorTimeout = 0, success with next block
+    await waitNewBlocks(1);
+    await transferExpectSuccess(collectionId, tokenId, Bob, Charlie);
+    const aliceBalanceAfterSponsoredTransaction1 = (await getFreeBalance(Alice)).toNumber();
+    expect(aliceBalanceAfterSponsoredTransaction1).to.be.lessThan(aliceBalanceBefore);
+  });
+});
+
+describe.only('Collection zero limits (Fungible)', () => {
+  let Alice: IKeyringPair;
+  let Bob: IKeyringPair;
+  let Charlie: IKeyringPair;
+
+  before(async () => {
+    await usingApi(async () => {
+      Alice = privateKey('//Alice');
+      Bob = privateKey('//Bob');
+      Charlie = privateKey('//Charlie');
+    });
+  });
+
+  it('Limits have 0 in sponsor timeout, no limits are applied', async () => {
+    const collectionId = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
+    await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 0 });
+    const tokenId = await createItemExpectSuccess(Alice, collectionId, 'Fungible');
+    await setCollectionSponsorExpectSuccess(collectionId, Alice.address);
+    await confirmSponsorshipExpectSuccess(collectionId, '//Alice');
+    await transferExpectSuccess(collectionId, tokenId, Alice, Bob, 10, 'Fungible');
+    const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber();
+    await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 2, 'Fungible');
+
+    // check setting SponsorTimeout = 0, success with next block
+    await waitNewBlocks(1);
+    await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 2, 'Fungible');
+    const aliceBalanceAfterSponsoredTransaction1 = (await getFreeBalance(Alice)).toNumber();
+    expect(aliceBalanceAfterSponsoredTransaction1).to.be.lessThan(aliceBalanceBefore);
+  });
+});
+
+describe.only('Collection zero limits (ReFungible)', () => {
+  let Alice: IKeyringPair;
+  let Bob: IKeyringPair;
+  let Charlie: IKeyringPair;
+
+  before(async () => {
+    await usingApi(async () => {
+      Alice = privateKey('//Alice');
+      Bob = privateKey('//Bob');
+      Charlie = privateKey('//Charlie');
+    });
+  });
+
+  it('Limits have 0 in tokens per address field, the chain limits are applied', async () => {  
+    const collectionId = await createCollectionExpectSuccess({mode: {type: 'ReFungible' }});
+    await setCollectionLimitsExpectSuccess(Alice, collectionId, { AccountTokenOwnershipLimit: 0 });
+    for(let i = 0; i < 10; i++){
+      await createItemExpectSuccess(Alice, collectionId, 'ReFungible');
+    }
+    await createItemExpectFailure(Alice, collectionId, 'ReFungible');
+  });
+
+  it('Limits have 0 in sponsor timeout, no limits are applied', async () => {
+
+    const collectionId = await createCollectionExpectSuccess({ mode: { type: 'ReFungible' } });
+    await setCollectionLimitsExpectSuccess(Alice, collectionId, { SponsorTimeout: 0 });
+    const tokenId = await createItemExpectSuccess(Alice, collectionId, 'ReFungible');
+    await setCollectionSponsorExpectSuccess(collectionId, Alice.address);
+    await confirmSponsorshipExpectSuccess(collectionId, '//Alice');
+    await transferExpectSuccess(collectionId, tokenId, Alice, Bob, 100, 'ReFungible');
+    await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 20, 'ReFungible');
+    const aliceBalanceBefore = (await getFreeBalance(Alice)).toNumber();
+
+    // check setting SponsorTimeout = 0, success with next block
+    await waitNewBlocks(1);
+    await transferExpectSuccess(collectionId, tokenId, Bob, Charlie, 20, 'ReFungible');
+    const aliceBalanceAfterSponsoredTransaction1 = (await getFreeBalance(Alice)).toNumber();
+    expect(aliceBalanceAfterSponsoredTransaction1).to.be.lessThan(aliceBalanceBefore);
+  });
+});
modifiedtests/src/util/helpers.tsdiffbeforeafterboth
--- a/tests/src/util/helpers.ts
+++ b/tests/src/util/helpers.ts
@@ -797,6 +797,17 @@
 }
 
 export async function
+getFreeBalance(account: IKeyringPair) : Promise<BigNumber>
+{
+  let balance = new BigNumber(0) ;
+  await usingApi(async (api) => { 
+    balance = new BigNumber((await api.query.system.account(account.address)).data.free.toString());  
+  });
+
+  return balance;
+}
+
+export async function
 scheduleTransferExpectSuccess(
   collectionId: number,
   tokenId: number,
@@ -870,8 +881,11 @@
     if (type === 'ReFungible') {
       const nftItemData =
         (await api.query.nft.reFungibleItemList(collectionId, tokenId)).toJSON() as unknown as IReFungibleTokenDataType;
-      expect(nftItemData.Owner[0].Owner).to.be.deep.equal(to);
-      expect(nftItemData.Owner[0].Fraction.toString()).to.be.equal(value.toString());
+      const expectedOwner = toSubstrateAddress(to);
+      const ownerIndex = nftItemData.Owner.findIndex(v => toSubstrateAddress(v.Owner as any as string) == expectedOwner);
+      expect(ownerIndex).to.not.equal(-1);
+      expect(nftItemData.Owner[ownerIndex].Owner).to.be.deep.equal(normalizeAccountId(to));
+      expect(nftItemData.Owner[ownerIndex].Fraction).to.be.greaterThanOrEqual(value as number);
     }
   });
 }
@@ -1171,4 +1185,21 @@
 
 export async function queryNftOwner(api: ApiPromise, collectionId: number, tokenId: number): Promise<CrossAccountId> {
   return normalizeAccountId((await api.query.nft.nftItemList(collectionId, tokenId) as any).toJSON().Owner);
-}
\ No newline at end of file
+}
+
+export async function waitNewBlocks(blocksCount = 1): Promise<void> {
+  await usingApi(async (api) => {
+    const promise = new Promise<void>(async (resolve) => {
+      
+      const unsubscribe = await api.rpc.chain.subscribeNewHeads(() => {
+        if (blocksCount > 0) {
+          blocksCount--;
+        } else {
+          unsubscribe();
+          resolve();
+        }
+      });
+    });
+    return promise;
+  });
+}