git.delta.rocks / unique-network / refs/commits / 0b44e9f57a8a

difftreelog

feat add collection sponsoring for rft

Grigoriy Simonov2022-12-23parent: #aea0773.patch.diff
in: master

4 files changed

modifiedruntime/common/config/pallets/app_promotion.rsdiffbeforeafterboth
--- a/runtime/common/config/pallets/app_promotion.rs
+++ b/runtime/common/config/pallets/app_promotion.rs
@@ -22,7 +22,7 @@
 use frame_support::{parameter_types, PalletId};
 use sp_arithmetic::Perbill;
 use up_common::{
-	constants::{UNIQUE, RELAY_DAYS, DAYS},
+	constants::{UNIQUE, RELAY_DAYS},
 	types::Balance,
 };
 
modifiedruntime/common/ethereum/sponsoring.rsdiffbeforeafterboth
--- a/runtime/common/ethereum/sponsoring.rs
+++ b/runtime/common/ethereum/sponsoring.rs
@@ -32,14 +32,22 @@
 	Config as FungibleConfig,
 	erc::{UniqueFungibleCall, ERC20Call},
 };
-use pallet_refungible::Config as RefungibleConfig;
+use pallet_refungible::{
+	Config as RefungibleConfig,
+	erc::UniqueRefungibleCall,
+	erc_token::{RefungibleTokenHandle, UniqueRefungibleTokenCall},
+	RefungibleHandle,
+};
 use pallet_unique::Config as UniqueConfig;
 use sp_std::prelude::*;
-use up_data_structs::{CollectionMode, CreateItemData, CreateNftData, TokenId};
+use up_data_structs::{
+	CollectionMode, CreateItemData, CreateNftData, mapping::TokenAddressMapping, TokenId,
+};
 use up_sponsorship::SponsorshipHandler;
-
 use crate::{Runtime, runtime_common::sponsoring::*};
 
+mod refungible;
+
 pub type EvmSponsorshipHandler = (
 	UniqueEthSponsorshipHandler<Runtime>,
 	pallet_evm_contract_helpers::HelpersContractSponsoring<Runtime>,
@@ -53,80 +61,155 @@
 		who: &T::CrossAccountId,
 		call_context: &CallContext,
 	) -> Option<T::CrossAccountId> {
-		let collection_id = map_eth_to_id(&call_context.contract_address)?;
-		let collection = <CollectionHandle<T>>::new(collection_id)?;
-		let sponsor = collection.sponsorship.sponsor()?.clone();
-		let (method_id, mut reader) = AbiReader::new_call(&call_context.input).ok()?;
-		Some(T::CrossAccountId::from_sub(match &collection.mode {
-			CollectionMode::NFT => {
-				let call = <UniqueNFTCall<T>>::parse(method_id, &mut reader).ok()??;
-				match call {
-					UniqueNFTCall::TokenProperties(TokenPropertiesCall::SetProperty {
-						token_id,
-						key,
-						value,
-						..
-					}) => {
-						let token_id: TokenId = token_id.try_into().ok()?;
-						withdraw_set_token_property::<T>(
+		if let Some(collection_id) = map_eth_to_id(&call_context.contract_address) {
+			let collection = <CollectionHandle<T>>::new(collection_id)?;
+			let sponsor = collection.sponsorship.sponsor()?.clone();
+			let (method_id, mut reader) = AbiReader::new_call(&call_context.input).ok()?;
+			Some(T::CrossAccountId::from_sub(match &collection.mode {
+				CollectionMode::NFT => {
+					let call = <UniqueNFTCall<T>>::parse(method_id, &mut reader).ok()??;
+					match call {
+						UniqueNFTCall::TokenProperties(TokenPropertiesCall::SetProperty {
+							token_id,
+							key,
+							value,
+							..
+						}) => {
+							let token_id: TokenId = token_id.try_into().ok()?;
+							withdraw_set_token_property::<T>(
+								&collection,
+								&who,
+								&token_id,
+								key.len() + value.len(),
+							)
+							.map(|()| sponsor)
+						}
+						UniqueNFTCall::ERC721UniqueExtensions(
+							ERC721UniqueExtensionsCall::Transfer { token_id, .. },
+						) => {
+							let token_id: TokenId = token_id.try_into().ok()?;
+							withdraw_transfer::<T>(&collection, &who, &token_id).map(|()| sponsor)
+						}
+						UniqueNFTCall::ERC721UniqueMintable(
+							ERC721UniqueMintableCall::Mint { .. }
+							| ERC721UniqueMintableCall::MintCheckId { .. }
+							| ERC721UniqueMintableCall::MintWithTokenUri { .. }
+							| ERC721UniqueMintableCall::MintWithTokenUriCheckId { .. },
+						) => withdraw_create_item::<T>(
 							&collection,
 							&who,
-							&token_id,
-							key.len() + value.len(),
+							&CreateItemData::NFT(CreateNftData::default()),
 						)
-						.map(|()| sponsor)
-					}
-					UniqueNFTCall::ERC721UniqueExtensions(
-						ERC721UniqueExtensionsCall::Transfer { token_id, .. },
-					) => {
-						let token_id: TokenId = token_id.try_into().ok()?;
-						withdraw_transfer::<T>(&collection, &who, &token_id).map(|()| sponsor)
-					}
-					UniqueNFTCall::ERC721UniqueMintable(
-						ERC721UniqueMintableCall::Mint { .. }
-						| ERC721UniqueMintableCall::MintCheckId { .. }
-						| ERC721UniqueMintableCall::MintWithTokenUri { .. }
-						| ERC721UniqueMintableCall::MintWithTokenUriCheckId { .. },
-					) => withdraw_create_item::<T>(
-						&collection,
-						&who,
-						&CreateItemData::NFT(CreateNftData::default()),
-					)
-					.map(|()| sponsor),
-					UniqueNFTCall::ERC721(ERC721Call::TransferFrom { token_id, from, .. }) => {
-						let token_id: TokenId = token_id.try_into().ok()?;
-						let from = T::CrossAccountId::from_eth(from);
-						withdraw_transfer::<T>(&collection, &from, &token_id).map(|()| sponsor)
-					}
-					UniqueNFTCall::ERC721(ERC721Call::Approve { token_id, .. }) => {
-						let token_id: TokenId = token_id.try_into().ok()?;
-						withdraw_approve::<T>(&collection, who.as_sub(), &token_id)
-							.map(|()| sponsor)
+						.map(|()| sponsor),
+						UniqueNFTCall::ERC721(ERC721Call::TransferFrom {
+							token_id, from, ..
+						}) => {
+							let token_id: TokenId = token_id.try_into().ok()?;
+							let from = T::CrossAccountId::from_eth(from);
+							withdraw_transfer::<T>(&collection, &from, &token_id).map(|()| sponsor)
+						}
+						UniqueNFTCall::ERC721(ERC721Call::Approve { token_id, .. }) => {
+							let token_id: TokenId = token_id.try_into().ok()?;
+							withdraw_approve::<T>(&collection, who.as_sub(), &token_id)
+								.map(|()| sponsor)
+						}
+						_ => None,
 					}
-					_ => None,
 				}
-			}
-			CollectionMode::Fungible(_) => {
-				let call = <UniqueFungibleCall<T>>::parse(method_id, &mut reader).ok()??;
-				#[allow(clippy::single_match)]
-				match call {
-					UniqueFungibleCall::ERC20(ERC20Call::Transfer { .. }) => {
-						withdraw_transfer::<T>(&collection, who, &TokenId::default())
-							.map(|()| sponsor)
-					}
-					UniqueFungibleCall::ERC20(ERC20Call::TransferFrom { from, .. }) => {
-						let from = T::CrossAccountId::from_eth(from);
-						withdraw_transfer::<T>(&collection, &from, &TokenId::default())
-							.map(|()| sponsor)
-					}
-					UniqueFungibleCall::ERC20(ERC20Call::Approve { .. }) => {
-						withdraw_approve::<T>(&collection, who.as_sub(), &TokenId::default())
-							.map(|()| sponsor)
+				CollectionMode::ReFungible => {
+					let call = <UniqueRefungibleCall<T>>::parse(method_id, &mut reader).ok()??;
+					refungible::call_sponsor(call, collection, who).map(|()| sponsor)
+				}
+				CollectionMode::Fungible(_) => {
+					let call = <UniqueFungibleCall<T>>::parse(method_id, &mut reader).ok()??;
+					match call {
+						UniqueFungibleCall::ERC20(ERC20Call::Transfer { .. }) => {
+							withdraw_transfer::<T>(&collection, who, &TokenId::default())
+								.map(|()| sponsor)
+						}
+						UniqueFungibleCall::ERC20(ERC20Call::TransferFrom { from, .. }) => {
+							let from = T::CrossAccountId::from_eth(from);
+							withdraw_transfer::<T>(&collection, &from, &TokenId::default())
+								.map(|()| sponsor)
+						}
+						UniqueFungibleCall::ERC20(ERC20Call::Approve { .. }) => {
+							withdraw_approve::<T>(&collection, who.as_sub(), &TokenId::default())
+								.map(|()| sponsor)
+						}
+						_ => None,
 					}
-					_ => None,
 				}
-			}
-			_ => None,
-		}?))
+			}?))
+		} else {
+			let (collection_id, token_id) =
+				T::EvmTokenAddressMapping::address_to_token(&call_context.contract_address)?;
+			let collection = <CollectionHandle<T>>::new(collection_id)?;
+			let sponsor = collection.sponsorship.sponsor()?.clone();
+			let rft_collection = RefungibleHandle::cast(collection);
+			let token = RefungibleTokenHandle(rft_collection, token_id);
+			let (method_id, mut reader) = AbiReader::new_call(&call_context.input).ok()?;
+			let call = <UniqueRefungibleTokenCall<T>>::parse(method_id, &mut reader).ok()??;
+			Some(T::CrossAccountId::from_sub(
+				refungible::token_call_sponsor(call, token, who).map(|()| sponsor)?,
+			))
+		}
+	}
+}
+
+mod common {
+	use super::*;
+
+	use pallet_common::erc::{CollectionCall};
+
+	pub fn collection_call_sponsor<T>(
+		call: CollectionCall<T>,
+		_collection: CollectionHandle<T>,
+		_who: &T::CrossAccountId,
+	) -> Option<()>
+	where
+		T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig,
+	{
+		use CollectionCall::*;
+
+		match call {
+			// Readonly
+			ERC165Call(_, _)
+			| CollectionProperty { .. }
+			| CollectionProperties { .. }
+			| HasCollectionPendingSponsor
+			| CollectionSponsor
+			| ContractAddress
+			| AllowlistedCross { .. }
+			| IsOwnerOrAdminEth { .. }
+			| IsOwnerOrAdminCross { .. }
+			| CollectionOwner
+			| CollectionAdmins
+			| UniqueCollectionType => None,
+
+			// Not sponsored
+			AddToCollectionAllowList { .. }
+			| AddToCollectionAllowListCross { .. }
+			| RemoveFromCollectionAllowList { .. }
+			| RemoveFromCollectionAllowListCross { .. }
+			| AddCollectionAdminCross { .. }
+			| RemoveCollectionAdminCross { .. }
+			| AddCollectionAdmin { .. }
+			| RemoveCollectionAdmin { .. }
+			| SetNestingBool { .. }
+			| SetNesting { .. }
+			| SetCollectionAccess { .. }
+			| SetCollectionMintMode { .. }
+			| SetOwner { .. }
+			| ChangeCollectionOwnerCross { .. }
+			| SetCollectionProperty { .. }
+			| SetCollectionProperties { .. }
+			| DeleteCollectionProperty { .. }
+			| DeleteCollectionProperties { .. }
+			| SetCollectionSponsor { .. }
+			| SetCollectionSponsorCross { .. }
+			| ConfirmCollectionSponsorship
+			| RemoveCollectionSponsor
+			| SetIntLimit { .. } => None,
+		}
 	}
 }
addedruntime/common/ethereum/sponsoring/refungible.rsdiffbeforeafterboth

no changes

modifiedtests/src/eth/collectionSponsoring.test.tsdiffbeforeafterboth
--- a/tests/src/eth/collectionSponsoring.test.ts
+++ b/tests/src/eth/collectionSponsoring.test.ts
@@ -15,10 +15,10 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds} from '../util/index';
+import {Pallets, requirePalletsOrSkip, usingPlaygrounds} from '../util/index';
 import {itEth, expect} from './util';
 
-describe('evm collection sponsoring', () => {
+describe('evm nft collection sponsoring', () => {
   let donor: IKeyringPair;
   let alice: IKeyringPair;
   let nominal: bigint;
@@ -317,3 +317,529 @@
     expect(collectionSponsor).to.deep.eq({Unconfirmed: sponsorSub.address});
   });
 });
+
+describe('evm RFT collection sponsoring', () => {
+  let donor: IKeyringPair;
+  let alice: IKeyringPair;
+  let nominal: bigint;
+
+  before(async function() {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);
+      donor = await privateKey({filename: __filename});
+      [alice] = await helper.arrange.createAccounts([100n], donor);
+      nominal = helper.balance.getOneTokenNominal();
+    });
+  });
+  
+  itEth('sponsors mint transactions', async ({helper}) => {
+    const collection = await helper.rft.mintCollection(alice, {tokenPrefix: 'spnr', permissions: {mintMode: true}});
+    await collection.setSponsor(alice, alice.address);
+    await collection.confirmSponsorship(alice);
+
+    const minter = helper.eth.createAccount();
+    expect(await helper.balance.getEthereum(minter)).to.equal(0n);
+
+    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);
+    const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', minter);
+
+    await collection.addToAllowList(alice, {Ethereum: minter});
+
+    const result = await contract.methods.mint(minter).send();
+
+    const events = helper.eth.normalizeEvents(result.events);
+    expect(events).to.deep.include({
+      address: collectionAddress,
+      event: 'Transfer',
+      args: {
+        from: '0x0000000000000000000000000000000000000000',
+        to: minter,
+        tokenId: '1',
+      },
+    });
+  });
+
+  // TODO: Temprorary off. Need refactor
+  // itWeb3('Set substrate sponsor', async ({api, web3, privateKeyWrapper}) => {
+  //   const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+  //   const collectionHelpers = evmCollectionHelpers(web3, owner);
+  //   let result = await collectionHelpers.methods.createRFTCollection('Sponsor collection', '1', '1').send();
+  //   const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result);
+  //   const sponsor = privateKeyWrapper('//Alice');
+  //   const collectionEvm = evmCollection(web3, owner, collectionIdAddress);
+
+  //   expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false;
+  //   result = await collectionEvm.methods.setCollectionSponsorSubstrate(sponsor.addressRaw).send({from: owner});
+  //   expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.true;
+
+  //   const confirmTx = await api.tx.unique.confirmSponsorship(collectionId);
+  //   await submitTransactionAsync(sponsor, confirmTx);
+  //   expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false;
+
+  //   const sponsorTuple = await collectionEvm.methods.collectionSponsor().call({from: owner});
+  //   expect(bigIntToSub(api, BigInt(sponsorTuple[1]))).to.be.eq(sponsor.address);
+  // });
+
+  // Soft-deprecated
+  itEth('[eth] Remove sponsor', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+    const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner);
+
+    let result = await collectionHelpers.methods.createRFTCollection('Sponsor collection', '1', '1').send({value: Number(2n * nominal)});
+    const collectionIdAddress = helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
+    const sponsor = await helper.eth.createAccountWithBalance(donor);
+    const collectionEvm = helper.ethNativeContract.collection(collectionIdAddress, 'rft', owner, true);
+
+    expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false;
+    result = await collectionEvm.methods.setCollectionSponsor(sponsor).send({from: owner});
+    expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.true;
+
+    await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor});
+    expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false;
+
+    await collectionEvm.methods.removeCollectionSponsor().send({from: owner});
+
+    const sponsorTuple = await collectionEvm.methods.collectionSponsor().call({from: owner});
+    expect(sponsorTuple.field_0).to.be.eq('0x0000000000000000000000000000000000000000');
+  });
+
+  itEth('[cross] Remove sponsor', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+    const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner);
+
+    let result = await collectionHelpers.methods.createRFTCollection('Sponsor collection', '1', '1').send({value: Number(2n * nominal)});
+    const collectionIdAddress = helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
+    const sponsor = await helper.eth.createAccountWithBalance(donor);
+    const sponsorCross = helper.ethCrossAccount.fromAddress(sponsor);
+    const collectionEvm = helper.ethNativeContract.collection(collectionIdAddress, 'rft', owner);
+
+    expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false;
+    result = await collectionEvm.methods.setCollectionSponsorCross(sponsorCross).send({from: owner});
+    expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.true;
+
+    await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor});
+    expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false;
+
+    await collectionEvm.methods.removeCollectionSponsor().send({from: owner});
+
+    const sponsorTuple = await collectionEvm.methods.collectionSponsor().call({from: owner});
+    expect(sponsorTuple.field_0).to.be.eq('0x0000000000000000000000000000000000000000');
+  });
+
+  // Soft-deprecated
+  itEth('[eth] Sponsoring collection from evm address via access list', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+
+    const {collectionId, collectionAddress} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner, 'Sponsor collection', '1', '1', '');
+
+    const collection = helper.rft.getCollectionObject(collectionId);
+    const sponsor = await helper.eth.createAccountWithBalance(donor);
+    const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner, true);
+
+    await collectionEvm.methods.setCollectionSponsor(sponsor).send({from: owner});
+    let collectionData = (await collection.getData())!;
+    expect(collectionData.raw.sponsorship.Unconfirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true));
+    await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor');
+
+    await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor});
+    collectionData = (await collection.getData())!;
+    expect(collectionData.raw.sponsorship.Confirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true));
+
+    const user = helper.eth.createAccount();
+    const nextTokenId = await collectionEvm.methods.nextTokenId().call();
+    expect(nextTokenId).to.be.equal('1');
+
+    const oldPermissions = (await collection.getData())!.raw.permissions; // (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman();
+    expect(oldPermissions.mintMode).to.be.false;
+    expect(oldPermissions.access).to.be.equal('Normal');
+
+    await collectionEvm.methods.setCollectionAccess(1 /*'AllowList'*/).send({from: owner});
+    await collectionEvm.methods.addToCollectionAllowList(user).send({from: owner});
+    await collectionEvm.methods.setCollectionMintMode(true).send({from: owner});
+
+    const newPermissions = (await collection.getData())!.raw.permissions; // (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman();
+    expect(newPermissions.mintMode).to.be.true;
+    expect(newPermissions.access).to.be.equal('AllowList');
+
+    const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
+    const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor));
+
+    {
+      const result = await collectionEvm.methods.mintWithTokenURI(user, 'Test URI').send({from: user});
+      const events = helper.eth.normalizeEvents(result.events);
+
+      expect(events).to.deep.include({
+        address: collectionAddress,
+        event: 'Transfer',
+        args: {
+          from: '0x0000000000000000000000000000000000000000',
+          to: user,
+          tokenId: '1',
+        },
+      });
+
+      const ownerBalanceAfter = await helper.balance.getSubstrate(await helper.address.ethToSubstrate(owner));
+      const sponsorBalanceAfter = await helper.balance.getSubstrate(await helper.address.ethToSubstrate(sponsor));
+
+      expect(await collectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI');
+      expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter);
+      expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true;
+    }
+  });
+
+  itEth('[cross] Sponsoring collection from evm address via access list', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+
+    const {collectionId, collectionAddress} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner, 'Sponsor collection', '1', '1', '');
+
+    const collection = helper.rft.getCollectionObject(collectionId);
+    const sponsor = await helper.eth.createAccountWithBalance(donor);
+    const sponsorCross = helper.ethCrossAccount.fromAddress(sponsor);
+    const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner);
+
+    await collectionEvm.methods.setCollectionSponsorCross(sponsorCross).send({from: owner});
+    let collectionData = (await collection.getData())!;
+    expect(collectionData.raw.sponsorship.Unconfirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true));
+    await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor');
+
+    await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor});
+    collectionData = (await collection.getData())!;
+    expect(collectionData.raw.sponsorship.Confirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true));
+
+    const user = helper.eth.createAccount();
+    const userCross = helper.ethCrossAccount.fromAddress(user);
+    const nextTokenId = await collectionEvm.methods.nextTokenId().call();
+    expect(nextTokenId).to.be.equal('1');
+
+    const oldPermissions = (await collection.getData())!.raw.permissions; // (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman();
+    expect(oldPermissions.mintMode).to.be.false;
+    expect(oldPermissions.access).to.be.equal('Normal');
+
+    await collectionEvm.methods.setCollectionAccess(1 /*'AllowList'*/).send({from: owner});
+    await collectionEvm.methods.addToCollectionAllowListCross(userCross).send({from: owner});
+    await collectionEvm.methods.setCollectionMintMode(true).send({from: owner});
+
+    const newPermissions = (await collection.getData())!.raw.permissions; // (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman();
+    expect(newPermissions.mintMode).to.be.true;
+    expect(newPermissions.access).to.be.equal('AllowList');
+
+    const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
+    const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor));
+
+    {
+      const result = await collectionEvm.methods.mintWithTokenURI(user, 'Test URI').send({from: user});
+      const events = helper.eth.normalizeEvents(result.events);
+
+      expect(events).to.deep.include({
+        address: collectionAddress,
+        event: 'Transfer',
+        args: {
+          from: '0x0000000000000000000000000000000000000000',
+          to: user,
+          tokenId: '1',
+        },
+      });
+
+      const ownerBalanceAfter = await helper.balance.getSubstrate(await helper.address.ethToSubstrate(owner));
+      const sponsorBalanceAfter = await helper.balance.getSubstrate(await helper.address.ethToSubstrate(sponsor));
+
+      expect(await collectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI');
+      expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter);
+      expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true;
+    }
+  });
+
+  // TODO: Temprorary off. Need refactor
+  // itWeb3('Sponsoring collection from substrate address via access list', async ({api, web3, privateKeyWrapper}) => {
+  //   const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+  //   const collectionHelpers = evmCollectionHelpers(web3, owner);
+  //   const result = await collectionHelpers.methods.createERC721MetadataCompatibleRFTCollection('Sponsor collection', '1', '1', '').send();
+  //   const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result);
+  //   const sponsor = privateKeyWrapper('//Alice');
+  //   const collectionEvm = evmCollection(web3, owner, collectionIdAddress);
+
+  //   await collectionEvm.methods.setCollectionSponsorSubstrate(sponsor.addressRaw).send({from: owner});
+
+  //   const confirmTx = await api.tx.unique.confirmSponsorship(collectionId);
+  //   await submitTransactionAsync(sponsor, confirmTx);
+
+  //   const user = createEthAccount(web3);
+  //   const nextTokenId = await collectionEvm.methods.nextTokenId().call();
+  //   expect(nextTokenId).to.be.equal('1');
+
+  //   await collectionEvm.methods.setCollectionAccess(1 /*'AllowList'*/).send({from: owner});
+  //   await collectionEvm.methods.addToCollectionAllowList(user).send({from: owner});
+  //   await collectionEvm.methods.setCollectionMintMode(true).send({from: owner});
+
+  //   const ownerBalanceBefore = await ethBalanceViaSub(api, owner);
+  //   const sponsorBalanceBefore = (await getBalance(api, [sponsor.address]))[0];
+
+  //   {
+  //     const nextTokenId = await collectionEvm.methods.nextTokenId().call();
+  //     expect(nextTokenId).to.be.equal('1');
+  //     const result = await collectionEvm.methods.mintWithTokenURI(
+  //       user,
+  //       nextTokenId,
+  //       'Test URI',
+  //     ).send({from: user});
+  //     const events = normalizeEvents(result.events);
+
+  //     expect(events).to.be.deep.equal([
+  //       {
+  //         address: collectionIdAddress,
+  //         event: 'Transfer',
+  //         args: {
+  //           from: '0x0000000000000000000000000000000000000000',
+  //           to: user,
+  //           tokenId: nextTokenId,
+  //         },
+  //       },
+  //     ]);
+
+  //     const ownerBalanceAfter = await ethBalanceViaSub(api, owner);
+  //     const sponsorBalanceAfter = (await getBalance(api, [sponsor.address]))[0];
+
+  //     expect(await collectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI');
+  //     expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter);
+  //     expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true;
+  //   }
+  // });
+
+  // Soft-deprecated
+  itEth('[eth] Check that transaction via EVM spend money from sponsor address', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+
+    const {collectionAddress, collectionId} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner,'Sponsor collection', '1', '1', '');
+    const collection = helper.rft.getCollectionObject(collectionId);
+    const sponsor = await helper.eth.createAccountWithBalance(donor);
+    const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner, true);
+
+    await collectionEvm.methods.setCollectionSponsor(sponsor).send();
+    let collectionData = (await collection.getData())!;
+    expect(collectionData.raw.sponsorship.Unconfirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true));
+    await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor');
+
+    const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor, true);
+    await sponsorCollection.methods.confirmCollectionSponsorship().send();
+    collectionData = (await collection.getData())!;
+    expect(collectionData.raw.sponsorship.Confirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true));
+
+    const user = helper.eth.createAccount();
+    await collectionEvm.methods.addCollectionAdmin(user).send();
+
+    const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
+    const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor));
+
+    const userCollectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', user, true);
+
+    const result = await userCollectionEvm.methods.mintWithTokenURI(user, 'Test URI').send();
+    const tokenId = result.events.Transfer.returnValues.tokenId;
+
+    const events = helper.eth.normalizeEvents(result.events);
+    const address = helper.ethAddress.fromCollectionId(collectionId);
+
+    expect(events).to.deep.include({
+      address,
+      event: 'Transfer',
+      args: {
+        from: '0x0000000000000000000000000000000000000000',
+        to: user,
+        tokenId: '1',
+      },
+    });
+    expect(await userCollectionEvm.methods.tokenURI(tokenId).call()).to.be.equal('Test URI');
+
+    const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
+    expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore);
+    const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor));
+    expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true;
+  });
+
+  itEth('[cross] Check that transaction via EVM spend money from sponsor address', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+
+    const {collectionAddress, collectionId} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner,'Sponsor collection', '1', '1', '');
+    const collection = helper.rft.getCollectionObject(collectionId);
+    const sponsor = await helper.eth.createAccountWithBalance(donor);
+    const sponsorCross = helper.ethCrossAccount.fromAddress(sponsor);
+    const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner);
+
+    await collectionEvm.methods.setCollectionSponsorCross(sponsorCross).send();
+    let collectionData = (await collection.getData())!;
+    expect(collectionData.raw.sponsorship.Unconfirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true));
+    await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor');
+
+    const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor);
+    await sponsorCollection.methods.confirmCollectionSponsorship().send();
+    collectionData = (await collection.getData())!;
+    expect(collectionData.raw.sponsorship.Confirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true));
+
+    const user = helper.eth.createAccount();
+    const userCross = helper.ethCrossAccount.fromAddress(user);
+    await collectionEvm.methods.addCollectionAdminCross(userCross).send();
+
+    const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
+    const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor));
+
+    const userCollectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', user);
+
+    const result = await userCollectionEvm.methods.mintWithTokenURI(user, 'Test URI').send();
+    const tokenId = result.events.Transfer.returnValues.tokenId;
+
+    const events = helper.eth.normalizeEvents(result.events);
+    const address = helper.ethAddress.fromCollectionId(collectionId);
+
+    expect(events).to.deep.include({
+      address,
+      event: 'Transfer',
+      args: {
+        from: '0x0000000000000000000000000000000000000000',
+        to: user,
+        tokenId: '1',
+      },
+    });
+    expect(await userCollectionEvm.methods.tokenURI(tokenId).call()).to.be.equal('Test URI');
+
+    const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
+    expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore);
+    const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor));
+    expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true;
+  });
+});
+
+describe('evm RFT token sponsoring', () => {
+  let donor: IKeyringPair;
+
+  before(async function() {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);
+      donor = await privateKey({filename: __filename});
+    });
+  });
+
+  itEth('[cross] Check that transfer via EVM spend money from sponsor address', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+
+    const {collectionAddress, collectionId} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner,'Sponsor collection', '1', '1', '');
+    const sponsor = await helper.eth.createAccountWithBalance(donor);
+    const sponsorCross = helper.ethCrossAccount.fromAddress(sponsor);
+    const receiver = await helper.eth.createAccountWithBalance(donor);
+    const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner);
+
+    await collectionEvm.methods.setCollectionSponsorCross(sponsorCross).send();
+
+    const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor);
+    await sponsorCollection.methods.confirmCollectionSponsorship().send();
+
+    const user = await helper.eth.createAccountWithBalance(donor);
+    const userCross = helper.ethCrossAccount.fromAddress(user);
+    await collectionEvm.methods.addCollectionAdminCross(userCross).send();
+
+    const userCollectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', user);
+
+    const result = await userCollectionEvm.methods.mintWithTokenURI(user, 'Test URI').send();
+    const tokenId = result.events.Transfer.returnValues.tokenId;
+
+    const tokenContract = helper.ethNativeContract.rftTokenById(collectionId, tokenId, user);    
+    await tokenContract.methods.repartition(2).send();
+    
+    const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
+    const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor));
+    const userBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user));
+
+    await tokenContract.methods.transfer(receiver, 1).send();
+
+    const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
+    expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore);
+    const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor));
+    expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true;
+    const userBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user));
+    expect(userBalanceAfter).to.be.eq(userBalanceBefore);
+  });
+
+  itEth('[cross] Check that approve via EVM spend money from sponsor address', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+
+    const {collectionAddress, collectionId} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner,'Sponsor collection', '1', '1', '');
+    const sponsor = await helper.eth.createAccountWithBalance(donor);
+    const sponsorCross = helper.ethCrossAccount.fromAddress(sponsor);
+    const receiver = await helper.eth.createAccountWithBalance(donor);
+    const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner);
+
+    await collectionEvm.methods.setCollectionSponsorCross(sponsorCross).send();
+
+    const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor);
+    await sponsorCollection.methods.confirmCollectionSponsorship().send();
+
+    const user = await helper.eth.createAccountWithBalance(donor);
+    const userCross = helper.ethCrossAccount.fromAddress(user);
+    await collectionEvm.methods.addCollectionAdminCross(userCross).send();
+
+    const userCollectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', user);
+
+    const result = await userCollectionEvm.methods.mintWithTokenURI(user, 'Test URI').send();
+    const tokenId = result.events.Transfer.returnValues.tokenId;
+
+    const tokenContract = helper.ethNativeContract.rftTokenById(collectionId, tokenId, user);    
+    await tokenContract.methods.repartition(2).send();
+    
+    const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
+    const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor));
+    const userBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user));
+
+    await tokenContract.methods.approve(receiver, 1).send();
+
+    const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
+    expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore);
+    const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor));
+    expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true;
+    const userBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user));
+    expect(userBalanceAfter).to.be.eq(userBalanceBefore);
+  });
+  
+
+  itEth('[cross] Check that transferFrom via EVM spend money from sponsor address', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+
+    const {collectionAddress, collectionId} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner,'Sponsor collection', '1', '1', '');
+    const sponsor = await helper.eth.createAccountWithBalance(donor);
+    const sponsorCross = helper.ethCrossAccount.fromAddress(sponsor);
+    const receiver = await helper.eth.createAccountWithBalance(donor);
+    const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner);
+
+    await collectionEvm.methods.setCollectionSponsorCross(sponsorCross).send();
+
+    const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor);
+    await sponsorCollection.methods.confirmCollectionSponsorship().send();
+
+    const user = await helper.eth.createAccountWithBalance(donor);
+    const userCross = helper.ethCrossAccount.fromAddress(user);
+    await collectionEvm.methods.addCollectionAdminCross(userCross).send();
+
+    const userCollectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', user);
+
+    const result = await userCollectionEvm.methods.mintWithTokenURI(user, 'Test URI').send();
+    const tokenId = result.events.Transfer.returnValues.tokenId;
+
+    const tokenContract = helper.ethNativeContract.rftTokenById(collectionId, tokenId, user);    
+    await tokenContract.methods.repartition(2).send();
+    await tokenContract.methods.approve(receiver, 1).send();
+    
+    const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
+    const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor));
+    const userBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user));
+    const receiverBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(receiver));
+
+    const receiverTokenContract = helper.ethNativeContract.rftTokenById(collectionId, tokenId, receiver);   
+    await receiverTokenContract.methods.transferFrom(user, receiver, 1).send();
+
+    const receiverBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(receiver));
+    expect(receiverBalanceAfter).to.be.eq(receiverBalanceBefore);
+    const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));
+    expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore);
+    const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor));
+    expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true;
+    const userBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user));
+    expect(userBalanceAfter).to.be.eq(userBalanceBefore);
+  });
+});