git.delta.rocks / unique-network / refs/commits / 3203150b5c74

difftreelog

Merge remote-tracking branch 'origin/feature/erc-721-for-refungible' into develop

Yaroslav Bolyukin2022-08-04parents: #f5cb854 #708f63e.patch.diff
in: master

13 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6311,7 +6311,7 @@
 
 [[package]]
 name = "pallet-refungible"
-version = "0.2.0"
+version = "0.2.1"
 dependencies = [
  "ethereum",
  "evm-coder",
modifiedMakefilediffbeforeafterboth
--- a/Makefile
+++ b/Makefile
@@ -15,8 +15,9 @@
 NONFUNGIBLE_EVM_STUBS=./pallets/nonfungible/src/stubs
 NONFUNGIBLE_EVM_ABI=./tests/src/eth/nonFungibleAbi.json
 
-RENFUNGIBLE_EVM_ABI=./tests/src/eth/reFungibleAbi.json
-RENFUNGIBLE_TOKEN_EVM_ABI=./tests/src/eth/reFungibleTokenAbi.json
+REFUNGIBLE_EVM_STUBS=./pallets/refungible/src/stubs
+REFUNGIBLE_EVM_ABI=./tests/src/eth/reFungibleAbi.json
+REFUNGIBLE_TOKEN_EVM_ABI=./tests/src/eth/reFungibleTokenAbi.json
 
 CONTRACT_HELPERS_STUBS=./pallets/evm-contract-helpers/src/stubs/
 CONTRACT_HELPERS_ABI=./tests/src/eth/util/contractHelpersAbi.json
@@ -36,7 +37,7 @@
 UniqueNFT.sol:
 	PACKAGE=pallet-nonfungible NAME=erc::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
 	PACKAGE=pallet-nonfungible NAME=erc::gen_impl OUTPUT=$(NONFUNGIBLE_EVM_STUBS)/$@ ./.maintain/scripts/generate_sol.sh
-	
+
 UniqueRefungible.sol:
 	PACKAGE=pallet-refungible NAME=erc::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
 	PACKAGE=pallet-refungible NAME=erc::gen_impl OUTPUT=$(REFUNGIBLE_EVM_STUBS)/$@ ./.maintain/scripts/generate_sol.sh
@@ -61,14 +62,14 @@
 	INPUT=$(NONFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(NONFUNGIBLE_EVM_STUBS)/UniqueNFT.raw ./.maintain/scripts/compile_stub.sh
 	INPUT=$(NONFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(NONFUNGIBLE_EVM_ABI) ./.maintain/scripts/generate_abi.sh
 
-UniqueRefungibleToken: UniqueRefungibleToken.sol
-	INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(REFUNGIBLE_EVM_STUBS)/UniqueRefungibleToken.raw ./.maintain/scripts/compile_stub.sh
-	INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(RENFUNGIBLE_TOKEN_EVM_ABI) ./.maintain/scripts/generate_abi.sh
-
 UniqueRefungible: UniqueRefungible.sol
 	INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(REFUNGIBLE_EVM_STUBS)/UniqueRefungible.raw ./.maintain/scripts/compile_stub.sh
 	INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(REFUNGIBLE_EVM_ABI) ./.maintain/scripts/generate_abi.sh
 
+UniqueRefungibleToken: UniqueRefungibleToken.sol
+	INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(REFUNGIBLE_EVM_STUBS)/UniqueRefungibleToken.raw ./.maintain/scripts/compile_stub.sh
+	INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(REFUNGIBLE_TOKEN_EVM_ABI) ./.maintain/scripts/generate_abi.sh
+
 ContractHelpers: ContractHelpers.sol
 	INPUT=$(CONTRACT_HELPERS_STUBS)/$< OUTPUT=$(CONTRACT_HELPERS_STUBS)/ContractHelpers.raw ./.maintain/scripts/compile_stub.sh
 	INPUT=$(CONTRACT_HELPERS_STUBS)/$< OUTPUT=$(CONTRACT_HELPERS_ABI) ./.maintain/scripts/generate_abi.sh
modifiedpallets/refungible/CHANGELOG.mddiffbeforeafterboth
22
3All notable changes to this project will be documented in this file.3All notable changes to this project will be documented in this file.
44
5## [v0.2.1] - 2022-07-27
6
7### New features
8
9Implementation of ERC-721 EVM API ([#452](https://github.com/UniqueNetwork/unique-chain/pull/452))
10
5## [v0.2.0] - 2022-08-0111## [v0.2.0] - 2022-08-01
12
6### Deprecated13### Deprecated
14
15`const_data` field is removed
16
7- `ItemData`17- `ItemData`
8- `TokenData`18- `TokenData`
919
15test(refungible-pallet): add tests for ERC-20 EVM API for RFT token pieces ([#413](https://github.com/UniqueNetwork/unique-chain/pull/413))25test(refungible-pallet): add tests for ERC-20 EVM API for RFT token pieces ([#413](https://github.com/UniqueNetwork/unique-chain/pull/413))
1626
17## [v0.1.1] - 2022-07-1427## [v0.1.1] - 2022-07-14
28
18### Added29### Added features
30
19- Support for properties for RFT collections and tokens.31- Support for properties for RFT collections and tokens.
2032
21### Other changes33### Other changes
2234
23- feat: RPC method `token_owners` returning 10 owners in no particular order.35- feat: RPC method `token_owners` returning 10 owners in no particular order.
2436
25This was an internal request to improve the web interface and support fractionalization event. 37This was an internal request to improve the web interface and support fractionalization event.
2638
modifiedpallets/refungible/Cargo.tomldiffbeforeafterboth
--- a/pallets/refungible/Cargo.toml
+++ b/pallets/refungible/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "pallet-refungible"
-version = "0.2.0"
+version = "0.2.1"
 license = "GPLv3"
 edition = "2021"
 
modifiedpallets/refungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -14,19 +14,797 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
+//! # Refungible Pallet EVM API for tokens
+//!
+//! Provides ERC-721 standart support implementation and EVM API for unique extensions for Refungible Pallet.
+//! Method implementations are mostly doing parameter conversion and calling Refungible Pallet methods.
+
 extern crate alloc;
-use evm_coder::{generate_stubgen, solidity_interface, types::*};
 
-use pallet_common::{CollectionHandle, erc::CollectionCall, erc::CommonEvmHandler};
+use alloc::string::ToString;
+use core::{
+	char::{REPLACEMENT_CHARACTER, decode_utf16},
+	convert::TryInto,
+};
+use evm_coder::{ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*, weight};
+use frame_support::{BoundedBTreeMap, BoundedVec};
+use pallet_common::{
+	CollectionHandle, CollectionPropertyPermissions,
+	erc::{
+		CommonEvmHandler, CollectionCall,
+		static_property::{key, value as property_value},
+	},
+};
+use pallet_evm::{account::CrossAccountId, PrecompileHandle};
+use pallet_evm_coder_substrate::{call, dispatch_to_evm};
+use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};
+use sp_core::H160;
+use sp_std::{collections::btree_map::BTreeMap, vec::Vec, vec};
+use up_data_structs::{
+	CollectionId, CollectionPropertiesVec, Property, PropertyKey, PropertyKeyPermission,
+	PropertyPermission, TokenId,
+};
+
+use crate::{
+	AccountBalance, Balance, Config, CreateItemData, Pallet, RefungibleHandle, SelfWeightOf,
+	TokenProperties, TokensMinted, TotalSupply, weights::WeightInfo,
+};
+
+pub const ADDRESS_FOR_PARTIALLY_OWNED_TOKENS: H160 = H160::repeat_byte(0xff);
+
+/// @title A contract that allows to set and delete token properties and change token property permissions.
+#[solidity_interface(name = "TokenProperties")]
+impl<T: Config> RefungibleHandle<T> {
+	/// @notice Set permissions for token property.
+	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
+	/// @param key Property key.
+	/// @param is_mutable Permission to mutate property.
+	/// @param collection_admin Permission to mutate property by collection admin if property is mutable.
+	/// @param token_owner Permission to mutate property by token owner if property is mutable.
+	fn set_token_property_permission(
+		&mut self,
+		caller: caller,
+		key: string,
+		is_mutable: bool,
+		collection_admin: bool,
+		token_owner: bool,
+	) -> Result<()> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		<Pallet<T>>::set_token_property_permissions(
+			self,
+			&caller,
+			vec![PropertyKeyPermission {
+				key: <Vec<u8>>::from(key)
+					.try_into()
+					.map_err(|_| "too long key")?,
+				permission: PropertyPermission {
+					mutable: is_mutable,
+					collection_admin,
+					token_owner,
+				},
+			}],
+		)
+		.map_err(dispatch_to_evm::<T>)
+	}
+
+	/// @notice Set token property value.
+	/// @dev Throws error if `msg.sender` has no permission to edit the property.
+	/// @param tokenId ID of the token.
+	/// @param key Property key.
+	/// @param value Property value.
+	fn set_property(
+		&mut self,
+		caller: caller,
+		token_id: uint256,
+		key: string,
+		value: bytes,
+	) -> Result<()> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
+		let key = <Vec<u8>>::from(key)
+			.try_into()
+			.map_err(|_| "key too long")?;
+		let value = value.try_into().map_err(|_| "value too long")?;
+
+		let nesting_budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		<Pallet<T>>::set_token_property(
+			self,
+			&caller,
+			TokenId(token_id),
+			Property { key, value },
+			&nesting_budget,
+		)
+		.map_err(dispatch_to_evm::<T>)
+	}
+
+	/// @notice Delete token property value.
+	/// @dev Throws error if `msg.sender` has no permission to edit the property.
+	/// @param tokenId ID of the token.
+	/// @param key Property key.
+	fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
+		let key = <Vec<u8>>::from(key)
+			.try_into()
+			.map_err(|_| "key too long")?;
+
+		let nesting_budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)
+			.map_err(dispatch_to_evm::<T>)
+	}
+
+	/// @notice Get token property value.
+	/// @dev Throws error if key not found
+	/// @param tokenId ID of the token.
+	/// @param key Property key.
+	/// @return Property value bytes
+	fn property(&self, token_id: uint256, key: string) -> Result<bytes> {
+		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
+		let key = <Vec<u8>>::from(key)
+			.try_into()
+			.map_err(|_| "key too long")?;
+
+		let props = <TokenProperties<T>>::get((self.id, token_id));
+		let prop = props.get(&key).ok_or("key not found")?;
+
+		Ok(prop.to_vec())
+	}
+}
+
+#[derive(ToLog)]
+pub enum ERC721Events {
+	/// @dev This event emits when NFTs are created (`from` == 0) and destroyed
+	///  (`to` == 0). Exception: during contract creation, any number of RFTs
+	///  may be created and assigned without emitting Transfer.
+	Transfer {
+		#[indexed]
+		from: address,
+		#[indexed]
+		to: address,
+		#[indexed]
+		token_id: uint256,
+	},
+	/// @dev Not supported
+	Approval {
+		#[indexed]
+		owner: address,
+		#[indexed]
+		approved: address,
+		#[indexed]
+		token_id: uint256,
+	},
+	/// @dev Not supported
+	#[allow(dead_code)]
+	ApprovalForAll {
+		#[indexed]
+		owner: address,
+		#[indexed]
+		operator: address,
+		approved: bool,
+	},
+}
+
+#[derive(ToLog)]
+pub enum ERC721MintableEvents {
+	/// @dev Not supported
+	#[allow(dead_code)]
+	MintingFinished {},
+}
+
+#[solidity_interface(name = "ERC721Metadata")]
+impl<T: Config> RefungibleHandle<T> {
+	/// @notice A descriptive name for a collection of RFTs in this contract
+	fn name(&self) -> Result<string> {
+		Ok(decode_utf16(self.name.iter().copied())
+			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))
+			.collect::<string>())
+	}
+
+	/// @notice An abbreviated name for RFTs in this contract
+	fn symbol(&self) -> Result<string> {
+		Ok(string::from_utf8_lossy(&self.token_prefix).into())
+	}
+
+	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
+	///
+	/// @dev If the token has a `url` property and it is not empty, it is returned.
+	///  Else If the collection does not have a property with key `schemaName` or its value is not equal to `ERC721Metadata`, it return an error `tokenURI not set`.
+	///  If the collection property `baseURI` is empty or absent, return "" (empty string)
+	///  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix
+	///  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).
+	///
+	/// @return token's const_metadata
+	#[solidity(rename_selector = "tokenURI")]
+	fn token_uri(&self, token_id: uint256) -> Result<string> {
+		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
+
+		if let Ok(url) = get_token_property(self, token_id_u32, &key::url()) {
+			if !url.is_empty() {
+				return Ok(url);
+			}
+		} else if !is_erc721_metadata_compatible::<T>(self.id) {
+			return Err("tokenURI not set".into());
+		}
+
+		if let Some(base_uri) =
+			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())
+		{
+			if !base_uri.is_empty() {
+				let base_uri = string::from_utf8(base_uri.into_inner()).map_err(|e| {
+					Error::Revert(alloc::format!(
+						"Can not convert value \"baseURI\" to string with error \"{}\"",
+						e
+					))
+				})?;
+				if let Ok(suffix) = get_token_property(self, token_id_u32, &key::suffix()) {
+					if !suffix.is_empty() {
+						return Ok(base_uri + suffix.as_str());
+					}
+				}
+
+				return Ok(base_uri + token_id.to_string().as_str());
+			}
+		}
+
+		Ok("".into())
+	}
+}
+
+/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
+/// @dev See https://eips.ethereum.org/EIPS/eip-721
+#[solidity_interface(name = "ERC721Enumerable")]
+impl<T: Config> RefungibleHandle<T> {
+	/// @notice Enumerate valid RFTs
+	/// @param index A counter less than `totalSupply()`
+	/// @return The token identifier for the `index`th NFT,
+	///  (sort order not specified)
+	fn token_by_index(&self, index: uint256) -> Result<uint256> {
+		Ok(index)
+	}
+
+	/// Not implemented
+	fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {
+		// TODO: Not implemetable
+		Err("not implemented".into())
+	}
+
+	/// @notice Count RFTs tracked by this contract
+	/// @return A count of valid RFTs tracked by this contract, where each one of
+	///  them has an assigned and queryable owner not equal to the zero address
+	fn total_supply(&self) -> Result<uint256> {
+		self.consume_store_reads(1)?;
+		Ok(<Pallet<T>>::total_supply(self).into())
+	}
+}
+
+/// @title ERC-721 Non-Fungible Token Standard
+/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
+#[solidity_interface(name = "ERC721", events(ERC721Events))]
+impl<T: Config> RefungibleHandle<T> {
+	/// @notice Count all RFTs assigned to an owner
+	/// @dev RFTs assigned to the zero address are considered invalid, and this
+	///  function throws for queries about the zero address.
+	/// @param owner An address for whom to query the balance
+	/// @return The number of RFTs owned by `owner`, possibly zero
+	fn balance_of(&self, owner: address) -> Result<uint256> {
+		self.consume_store_reads(1)?;
+		let owner = T::CrossAccountId::from_eth(owner);
+		let balance = <AccountBalance<T>>::get((self.id, owner));
+		Ok(balance.into())
+	}
+
+	/// @notice Find the owner of an RFT
+	/// @dev RFTs assigned to zero address are considered invalid, and queries
+	///  about them do throw.
+	///  Returns special 0xffffffffffffffffffffffffffffffffffffffff address for
+	///  the tokens that are partially owned.
+	/// @param tokenId The identifier for an RFT
+	/// @return The address of the owner of the RFT
+	fn owner_of(&self, token_id: uint256) -> Result<address> {
+		self.consume_store_reads(2)?;
+		let token = token_id.try_into()?;
+		let owner = <Pallet<T>>::token_owner(self.id, token);
+		Ok(owner
+			.map(|address| *address.as_eth())
+			.unwrap_or_else(|| ADDRESS_FOR_PARTIALLY_OWNED_TOKENS))
+	}
+
+	/// @dev Not implemented
+	fn safe_transfer_from_with_data(
+		&mut self,
+		_from: address,
+		_to: address,
+		_token_id: uint256,
+		_data: bytes,
+		_value: value,
+	) -> Result<void> {
+		// TODO: Not implemetable
+		Err("not implemented".into())
+	}
+
+	/// @dev Not implemented
+	fn safe_transfer_from(
+		&mut self,
+		_from: address,
+		_to: address,
+		_token_id: uint256,
+		_value: value,
+	) -> Result<void> {
+		// TODO: Not implemetable
+		Err("not implemented".into())
+	}
+
+	/// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE
+	///  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE
+	///  THEY MAY BE PERMANENTLY LOST
+	/// @dev Throws unless `msg.sender` is the current owner or an authorized
+	///  operator for this RFT. Throws if `from` is not the current owner. Throws
+	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.
+	///  Throws if RFT pieces have multiple owners.
+	/// @param from The current owner of the NFT
+	/// @param to The new owner
+	/// @param tokenId The NFT to transfer
+	/// @param _value Not used for an NFT
+	#[weight(<SelfWeightOf<T>>::transfer_from_creating_removing())]
+	fn transfer_from(
+		&mut self,
+		caller: caller,
+		from: address,
+		to: address,
+		token_id: uint256,
+		_value: value,
+	) -> Result<void> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let from = T::CrossAccountId::from_eth(from);
+		let to = T::CrossAccountId::from_eth(to);
+		let token = token_id.try_into()?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		let balance = balance(&self, token, &from)?;
+		ensure_single_owner(&self, token, balance)?;
+
+		<Pallet<T>>::transfer_from(self, &caller, &from, &to, token, balance, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+
+		Ok(())
+	}
+
+	/// @dev Not implemented
+	fn approve(
+		&mut self,
+		_caller: caller,
+		_approved: address,
+		_token_id: uint256,
+		_value: value,
+	) -> Result<void> {
+		Err("not implemented".into())
+	}
+
+	/// @dev Not implemented
+	fn set_approval_for_all(
+		&mut self,
+		_caller: caller,
+		_operator: address,
+		_approved: bool,
+	) -> Result<void> {
+		// TODO: Not implemetable
+		Err("not implemented".into())
+	}
+
+	/// @dev Not implemented
+	fn get_approved(&self, _token_id: uint256) -> Result<address> {
+		// TODO: Not implemetable
+		Err("not implemented".into())
+	}
+
+	/// @dev Not implemented
+	fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {
+		// TODO: Not implemetable
+		Err("not implemented".into())
+	}
+}
+
+/// Returns amount of pieces of `token` that `owner` have
+fn balance<T: Config>(
+	collection: &RefungibleHandle<T>,
+	token: TokenId,
+	owner: &T::CrossAccountId,
+) -> Result<u128> {
+	collection.consume_store_reads(1)?;
+	let balance = <Balance<T>>::get((collection.id, token, &owner));
+	Ok(balance)
+}
+
+/// Throws if `owner_balance` is lower than total amount of `token` pieces
+fn ensure_single_owner<T: Config>(
+	collection: &RefungibleHandle<T>,
+	token: TokenId,
+	owner_balance: u128,
+) -> Result<()> {
+	collection.consume_store_reads(1)?;
+	let total_supply = <TotalSupply<T>>::get((collection.id, token));
+	if total_supply != owner_balance {
+		return Err("token has multiple owners".into());
+	}
+	Ok(())
+}
+
+/// @title ERC721 Token that can be irreversibly burned (destroyed).
+#[solidity_interface(name = "ERC721Burnable")]
+impl<T: Config> RefungibleHandle<T> {
+	/// @notice Burns a specific ERC721 token.
+	/// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized
+	///  operator of the current owner.
+	/// @param tokenId The RFT to approve
+	#[weight(<SelfWeightOf<T>>::burn_item_fully())]
+	fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let token = token_id.try_into()?;
+
+		let balance = balance(&self, token, &caller)?;
+		ensure_single_owner(&self, token, balance)?;
+
+		<Pallet<T>>::burn(self, &caller, token, balance).map_err(dispatch_to_evm::<T>)?;
+		Ok(())
+	}
+}
+
+/// @title ERC721 minting logic.
+#[solidity_interface(name = "ERC721Mintable", events(ERC721MintableEvents))]
+impl<T: Config> RefungibleHandle<T> {
+	fn minting_finished(&self) -> Result<bool> {
+		Ok(false)
+	}
+
+	/// @notice Function to mint token.
+	/// @dev `tokenId` should be obtained with `nextTokenId` method,
+	///  unlike standard, you can't specify it manually
+	/// @param to The new owner
+	/// @param tokenId ID of the minted RFT
+	#[weight(<SelfWeightOf<T>>::create_item())]
+	fn mint(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = T::CrossAccountId::from_eth(to);
+		let token_id: u32 = token_id.try_into()?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		if <TokensMinted<T>>::get(self.id)
+			.checked_add(1)
+			.ok_or("item id overflow")?
+			!= token_id
+		{
+			return Err("item id should be next".into());
+		}
+
+		let users = [(to.clone(), 1)]
+			.into_iter()
+			.collect::<BTreeMap<_, _>>()
+			.try_into()
+			.unwrap();
+		<Pallet<T>>::create_item(
+			self,
+			&caller,
+			CreateItemData::<T> {
+				users,
+				properties: CollectionPropertiesVec::default(),
+			},
+			&budget,
+		)
+		.map_err(dispatch_to_evm::<T>)?;
+
+		Ok(true)
+	}
+
+	/// @notice Function to mint token with the given tokenUri.
+	/// @dev `tokenId` should be obtained with `nextTokenId` method,
+	///  unlike standard, you can't specify it manually
+	/// @param to The new owner
+	/// @param tokenId ID of the minted RFT
+	/// @param tokenUri Token URI that would be stored in the RFT properties
+	#[solidity(rename_selector = "mintWithTokenURI")]
+	#[weight(<SelfWeightOf<T>>::create_item())]
+	fn mint_with_token_uri(
+		&mut self,
+		caller: caller,
+		to: address,
+		token_id: uint256,
+		token_uri: string,
+	) -> Result<bool> {
+		let key = key::url();
+		let permission = get_token_permission::<T>(self.id, &key)?;
+		if !permission.collection_admin {
+			return Err("Operation is not allowed".into());
+		}
+
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = T::CrossAccountId::from_eth(to);
+		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
 
-use pallet_evm::PrecompileHandle;
-use pallet_evm_coder_substrate::call;
+		if <TokensMinted<T>>::get(self.id)
+			.checked_add(1)
+			.ok_or("item id overflow")?
+			!= token_id
+		{
+			return Err("item id should be next".into());
+		}
 
-use crate::{Config, RefungibleHandle};
+		let mut properties = CollectionPropertiesVec::default();
+		properties
+			.try_push(Property {
+				key,
+				value: token_uri
+					.into_bytes()
+					.try_into()
+					.map_err(|_| "token uri is too long")?,
+			})
+			.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;
 
+		let users = [(to.clone(), 1)]
+			.into_iter()
+			.collect::<BTreeMap<_, _>>()
+			.try_into()
+			.unwrap();
+		<Pallet<T>>::create_item(
+			self,
+			&caller,
+			CreateItemData::<T> {
+				users,
+				properties,
+			},
+			&budget,
+		)
+		.map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+
+	/// @dev Not implemented
+	fn finish_minting(&mut self, _caller: caller) -> Result<bool> {
+		Err("not implementable".into())
+	}
+}
+
+fn get_token_property<T: Config>(
+	collection: &CollectionHandle<T>,
+	token_id: u32,
+	key: &up_data_structs::PropertyKey,
+) -> Result<string> {
+	collection.consume_store_reads(1)?;
+	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))
+		.map_err(|_| Error::Revert("Token properties not found".into()))?;
+	if let Some(property) = properties.get(key) {
+		return Ok(string::from_utf8_lossy(property).into());
+	}
+
+	Err("Property tokenURI not found".into())
+}
+
+fn is_erc721_metadata_compatible<T: Config>(collection_id: CollectionId) -> bool {
+	if let Some(shema_name) =
+		pallet_common::Pallet::<T>::get_collection_property(collection_id, &key::schema_name())
+	{
+		let shema_name = shema_name.into_inner();
+		shema_name == property_value::ERC721_METADATA
+	} else {
+		false
+	}
+}
+
+fn get_token_permission<T: Config>(
+	collection_id: CollectionId,
+	key: &PropertyKey,
+) -> Result<PropertyPermission> {
+	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)
+		.map_err(|_| Error::Revert("No permissions for collection".into()))?;
+	let a = token_property_permissions
+		.get(key)
+		.map(Clone::clone)
+		.ok_or_else(|| {
+			let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();
+			Error::Revert(alloc::format!("No permission for key {}", key))
+		})?;
+	Ok(a)
+}
+
+/// @title Unique extensions for ERC721.
+#[solidity_interface(name = "ERC721UniqueExtensions")]
+impl<T: Config> RefungibleHandle<T> {
+	/// @notice Transfer ownership of an RFT
+	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`
+	///  is the zero address. Throws if `tokenId` is not a valid RFT.
+	///  Throws if RFT pieces have multiple owners.
+	/// @param to The new owner
+	/// @param tokenId The RFT to transfer
+	/// @param _value Not used for an RFT
+	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]
+	fn transfer(
+		&mut self,
+		caller: caller,
+		to: address,
+		token_id: uint256,
+		_value: value,
+	) -> Result<void> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = T::CrossAccountId::from_eth(to);
+		let token = token_id.try_into()?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		let balance = balance(&self, token, &caller)?;
+		ensure_single_owner(&self, token, balance)?;
+
+		<Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(())
+	}
+
+	/// @notice Burns a specific ERC721 token.
+	/// @dev Throws unless `msg.sender` is the current owner or an authorized
+	///  operator for this RFT. Throws if `from` is not the current owner. Throws
+	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.
+	///  Throws if RFT pieces have multiple owners.
+	/// @param from The current owner of the RFT
+	/// @param tokenId The RFT to transfer
+	/// @param _value Not used for an RFT
+	#[weight(<SelfWeightOf<T>>::burn_from())]
+	fn burn_from(
+		&mut self,
+		caller: caller,
+		from: address,
+		token_id: uint256,
+		_value: value,
+	) -> Result<void> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let from = T::CrossAccountId::from_eth(from);
+		let token = token_id.try_into()?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		let balance = balance(&self, token, &caller)?;
+		ensure_single_owner(&self, token, balance)?;
+
+		<Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(())
+	}
+
+	/// @notice Returns next free RFT ID.
+	fn next_token_id(&self) -> Result<uint256> {
+		self.consume_store_reads(1)?;
+		Ok(<TokensMinted<T>>::get(self.id)
+			.checked_add(1)
+			.ok_or("item id overflow")?
+			.into())
+	}
+
+	/// @notice Function to mint multiple tokens.
+	/// @dev `tokenIds` should be an array of consecutive numbers and first number
+	///  should be obtained with `nextTokenId` method
+	/// @param to The new owner
+	/// @param tokenIds IDs of the minted RFTs
+	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]
+	fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = T::CrossAccountId::from_eth(to);
+		let mut expected_index = <TokensMinted<T>>::get(self.id)
+			.checked_add(1)
+			.ok_or("item id overflow")?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		let total_tokens = token_ids.len();
+		for id in token_ids.into_iter() {
+			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;
+			if id != expected_index {
+				return Err("item id should be next".into());
+			}
+			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;
+		}
+		let users = [(to.clone(), 1)]
+			.into_iter()
+			.collect::<BTreeMap<_, _>>()
+			.try_into()
+			.unwrap();
+		let create_item_data = CreateItemData::<T> {
+			users,
+			properties: CollectionPropertiesVec::default(),
+		};
+		let data = (0..total_tokens)
+			.map(|_| create_item_data.clone())
+			.collect();
+
+		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+
+	/// @notice Function to mint multiple tokens with the given tokenUris.
+	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive
+	///  numbers and first number should be obtained with `nextTokenId` method
+	/// @param to The new owner
+	/// @param tokens array of pairs of token ID and token URI for minted tokens
+	#[solidity(rename_selector = "mintBulkWithTokenURI")]
+	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]
+	fn mint_bulk_with_token_uri(
+		&mut self,
+		caller: caller,
+		to: address,
+		tokens: Vec<(uint256, string)>,
+	) -> Result<bool> {
+		let key = key::url();
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = T::CrossAccountId::from_eth(to);
+		let mut expected_index = <TokensMinted<T>>::get(self.id)
+			.checked_add(1)
+			.ok_or("item id overflow")?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		let mut data = Vec::with_capacity(tokens.len());
+		let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]
+			.into_iter()
+			.collect::<BTreeMap<_, _>>()
+			.try_into()
+			.unwrap();
+		for (id, token_uri) in tokens {
+			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;
+			if id != expected_index {
+				return Err("item id should be next".into());
+			}
+			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;
+
+			let mut properties = CollectionPropertiesVec::default();
+			properties
+				.try_push(Property {
+					key: key.clone(),
+					value: token_uri
+						.into_bytes()
+						.try_into()
+						.map_err(|_| "token uri is too long")?,
+				})
+				.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;
+
+			let create_item_data = CreateItemData::<T> {
+				users: users.clone(),
+				properties,
+			};
+			data.push(create_item_data);
+		}
+
+		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+}
+
 #[solidity_interface(
 	name = "UniqueRefungible",
-	is(via("CollectionHandle<T>", common_mut, Collection),)
+	is(
+		ERC721,
+		ERC721Metadata,
+		ERC721Enumerable,
+		ERC721UniqueExtensions,
+		ERC721Mintable,
+		ERC721Burnable,
+		via("CollectionHandle<T>", common_mut, Collection),
+		TokenProperties,
+	)
 )]
 impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> {}
 
modifiedpallets/refungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -88,6 +88,7 @@
 #![cfg_attr(not(feature = "std"), no_std)]
 
 use crate::erc_token::ERC20Events;
+use crate::erc::ERC721Events;
 
 use codec::{Encode, Decode, MaxEncodedLen};
 use core::ops::Deref;
@@ -96,7 +97,8 @@
 use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};
 use pallet_evm_coder_substrate::WithRecorder;
 use pallet_common::{
-	CommonCollectionOperations, Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,
+	CommonCollectionOperations, Error as CommonError, Event as CommonEvent,
+	eth::collection_id_to_address, Pallet as PalletCommon,
 };
 use pallet_structure::Pallet as PalletStructure;
 use scale_info::TypeInfo;
@@ -117,6 +119,9 @@
 pub mod erc;
 pub mod erc_token;
 pub mod weights;
+
+pub type CreateItemData<T> =
+	CreateRefungibleExData<<T as pallet_evm::account::Config>::CrossAccountId>;
 pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;
 
 /// Token data, stored independently from other data used to describe it
@@ -396,6 +401,7 @@
 
 	pub fn burn_token_unchecked(
 		collection: &RefungibleHandle<T>,
+		owner: &T::CrossAccountId,
 		token_id: TokenId,
 	) -> DispatchResult {
 		let burnt = <TokensBurnt<T>>::get(collection.id)
@@ -407,7 +413,15 @@
 		<TotalSupply<T>>::remove((collection.id, token_id));
 		<Balance<T>>::remove_prefix((collection.id, token_id), None);
 		<Allowance<T>>::remove_prefix((collection.id, token_id), None);
-		// TODO: ERC721 transfer event
+
+		<PalletEvm<T>>::deposit_log(
+			ERC721Events::Transfer {
+				from: *owner.as_eth(),
+				to: H160::default(),
+				token_id: token_id.into(),
+			}
+			.to_log(collection_id_to_address(collection.id)),
+		);
 		Ok(())
 	}
 
@@ -449,7 +463,15 @@
 			<Owned<T>>::remove((collection.id, owner, token));
 			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);
 			<AccountBalance<T>>::insert((collection.id, owner), account_balance);
-			Self::burn_token_unchecked(collection, token)?;
+			Self::burn_token_unchecked(collection, owner, token)?;
+			<PalletEvm<T>>::deposit_log(
+				ERC20Events::Transfer {
+					from: *owner.as_eth(),
+					to: H160::default(),
+					value: amount.into(),
+				}
+				.to_log(collection_id_to_address(collection.id)),
+			);
 			<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(
 				collection.id,
 				token,
@@ -478,6 +500,17 @@
 			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);
 			<Balance<T>>::remove((collection.id, token, owner));
 			<AccountBalance<T>>::insert((collection.id, owner), account_balance);
+
+			if let Some(user) = Self::token_owner(collection.id, token) {
+				<PalletEvm<T>>::deposit_log(
+					ERC721Events::Transfer {
+						from: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,
+						to: *user.as_eth(),
+						token_id: token.into(),
+					}
+					.to_log(collection_id_to_address(collection.id)),
+				);
+			}
 		} else {
 			<Balance<T>>::insert((collection.id, token, owner), balance);
 		}
@@ -695,12 +728,13 @@
 		}
 		<PalletCommon<T>>::ensure_correct_receiver(to)?;
 
-		let balance_from = <Balance<T>>::get((collection.id, token, from))
+		let initial_balance_from = <Balance<T>>::get((collection.id, token, from));
+		let updated_balance_from = initial_balance_from
 			.checked_sub(amount)
 			.ok_or(<CommonError<T>>::TokenValueTooLow)?;
 		let mut create_target = false;
 		let from_to_differ = from != to;
-		let balance_to = if from != to {
+		let updated_balance_to = if from != to {
 			let old_balance = <Balance<T>>::get((collection.id, token, to));
 			if old_balance == 0 {
 				create_target = true;
@@ -714,7 +748,7 @@
 			None
 		};
 
-		let account_balance_from = if balance_from == 0 {
+		let account_balance_from = if updated_balance_from == 0 {
 			Some(
 				<AccountBalance<T>>::get((collection.id, from))
 					.checked_sub(1)
@@ -750,15 +784,15 @@
 			nesting_budget,
 		)?;
 
-		if let Some(balance_to) = balance_to {
+		if let Some(updated_balance_to) = updated_balance_to {
 			// from != to
-			if balance_from == 0 {
+			if updated_balance_from == 0 {
 				<Balance<T>>::remove((collection.id, token, from));
 				<PalletStructure<T>>::unnest_if_nested(from, collection.id, token);
 			} else {
-				<Balance<T>>::insert((collection.id, token, from), balance_from);
+				<Balance<T>>::insert((collection.id, token, from), updated_balance_from);
 			}
-			<Balance<T>>::insert((collection.id, token, to), balance_to);
+			<Balance<T>>::insert((collection.id, token, to), updated_balance_to);
 			if let Some(account_balance_from) = account_balance_from {
 				<AccountBalance<T>>::insert((collection.id, from), account_balance_from);
 				<Owned<T>>::remove((collection.id, from, token));
@@ -780,6 +814,7 @@
 				token,
 			)),
 		);
+
 		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(
 			collection.id,
 			token,
@@ -787,6 +822,46 @@
 			to.clone(),
 			amount,
 		));
+
+		let total_supply = <TotalSupply<T>>::get((collection.id, token));
+
+		if amount == total_supply {
+			// if token was fully owned by `from` and will be fully owned by `to` after transfer
+			<PalletEvm<T>>::deposit_log(
+				ERC721Events::Transfer {
+					from: *from.as_eth(),
+					to: *to.as_eth(),
+					token_id: token.into(),
+				}
+				.to_log(collection_id_to_address(collection.id)),
+			);
+		} else if let Some(updated_balance_to) = updated_balance_to {
+			// if `from` not equals `to`. This condition is needed to avoid sending event
+			// when `from` fully owns token and sends part of token pieces to itself.
+			if initial_balance_from == total_supply {
+				// if token was fully owned by `from` and will be only partially owned by `to`
+				// and `from` after transfer
+				<PalletEvm<T>>::deposit_log(
+					ERC721Events::Transfer {
+						from: *from.as_eth(),
+						to: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,
+						token_id: token.into(),
+					}
+					.to_log(collection_id_to_address(collection.id)),
+				);
+			} else if updated_balance_to == total_supply {
+				// if token was partially owned by `from` and will be fully owned by `to` after transfer
+				<PalletEvm<T>>::deposit_log(
+					ERC721Events::Transfer {
+						from: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,
+						to: *to.as_eth(),
+						token_id: token.into(),
+					}
+					.to_log(collection_id_to_address(collection.id)),
+				);
+			}
+		}
+
 		Ok(())
 	}
 
@@ -798,7 +873,7 @@
 	pub fn create_multiple_items(
 		collection: &RefungibleHandle<T>,
 		sender: &T::CrossAccountId,
-		data: Vec<CreateRefungibleExData<T::CrossAccountId>>,
+		data: Vec<CreateItemData<T>>,
 		nesting_budget: &dyn Budget,
 	) -> DispatchResult {
 		if !collection.is_owner_or_admin(sender) {
@@ -920,11 +995,35 @@
 		for (i, token) in data.into_iter().enumerate() {
 			let token_id = first_token_id + i as u32 + 1;
 
-			for (user, amount) in token.users.into_iter() {
-				if amount == 0 {
-					continue;
-				}
+			let receivers = token
+				.users
+				.into_iter()
+				.filter(|(_, amount)| *amount > 0)
+				.collect::<Vec<_>>();
+
+			if let [(user, _)] = receivers.as_slice() {
+				// if there is exactly one receiver
+				<PalletEvm<T>>::deposit_log(
+					ERC721Events::Transfer {
+						from: H160::default(),
+						to: *user.as_eth(),
+						token_id: token_id.into(),
+					}
+					.to_log(collection_id_to_address(collection.id)),
+				);
+			} else if let [_, ..] = receivers.as_slice() {
+				// if there is more than one receiver
+				<PalletEvm<T>>::deposit_log(
+					ERC721Events::Transfer {
+						from: H160::default(),
+						to: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,
+						token_id: token_id.into(),
+					}
+					.to_log(collection_id_to_address(collection.id)),
+				);
+			}
 
+			for (user, amount) in receivers.into_iter() {
 				<PalletEvm<T>>::deposit_log(
 					ERC20Events::Transfer {
 						from: H160::default(),
@@ -1114,7 +1213,7 @@
 	pub fn create_item(
 		collection: &RefungibleHandle<T>,
 		sender: &T::CrossAccountId,
-		data: CreateRefungibleExData<T::CrossAccountId>,
+		data: CreateItemData<T>,
 		nesting_budget: &dyn Budget,
 	) -> DispatchResult {
 		Self::create_multiple_items(collection, sender, vec![data], nesting_budget)
modifiedpallets/refungible/src/stubs/UniqueRefungible.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/refungible/src/stubs/UniqueRefungible.soldiffbeforeafterboth
--- a/pallets/refungible/src/stubs/UniqueRefungible.sol
+++ b/pallets/refungible/src/stubs/UniqueRefungible.sol
@@ -3,6 +3,12 @@
 
 pragma solidity >=0.8.0 <0.9.0;
 
+// Anonymous struct
+struct Tuple0 {
+	uint256 field_0;
+	string field_1;
+}
+
 // Common stubs holder
 contract Dummy {
 	uint8 dummy;
@@ -21,6 +27,392 @@
 	}
 }
 
+// Inline
+contract ERC721Events {
+	event Transfer(
+		address indexed from,
+		address indexed to,
+		uint256 indexed tokenId
+	);
+	event Approval(
+		address indexed owner,
+		address indexed approved,
+		uint256 indexed tokenId
+	);
+	event ApprovalForAll(
+		address indexed owner,
+		address indexed operator,
+		bool approved
+	);
+}
+
+// Inline
+contract ERC721MintableEvents {
+	event MintingFinished();
+}
+
+// Selector: 41369377
+contract TokenProperties is Dummy, ERC165 {
+	// @notice Set permissions for token property.
+	// @dev Throws error if `msg.sender` is not admin or owner of the collection.
+	// @param key Property key.
+	// @param is_mutable Permission to mutate property.
+	// @param collection_admin Permission to mutate property by collection admin if property is mutable.
+	// @param token_owner Permission to mutate property by token owner if property is mutable.
+	//
+	// Selector: setTokenPropertyPermission(string,bool,bool,bool) 222d97fa
+	function setTokenPropertyPermission(
+		string memory key,
+		bool isMutable,
+		bool collectionAdmin,
+		bool tokenOwner
+	) public {
+		require(false, stub_error);
+		key;
+		isMutable;
+		collectionAdmin;
+		tokenOwner;
+		dummy = 0;
+	}
+
+	// @notice Set token property value.
+	// @dev Throws error if `msg.sender` has no permission to edit the property.
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	// @param value Property value.
+	//
+	// Selector: setProperty(uint256,string,bytes) 1752d67b
+	function setProperty(
+		uint256 tokenId,
+		string memory key,
+		bytes memory value
+	) public {
+		require(false, stub_error);
+		tokenId;
+		key;
+		value;
+		dummy = 0;
+	}
+
+	// @notice Delete token property value.
+	// @dev Throws error if `msg.sender` has no permission to edit the property.
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	//
+	// Selector: deleteProperty(uint256,string) 066111d1
+	function deleteProperty(uint256 tokenId, string memory key) public {
+		require(false, stub_error);
+		tokenId;
+		key;
+		dummy = 0;
+	}
+
+	// @notice Get token property value.
+	// @dev Throws error if key not found
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	// @return Property value bytes
+	//
+	// Selector: property(uint256,string) 7228c327
+	function property(uint256 tokenId, string memory key)
+		public
+		view
+		returns (bytes memory)
+	{
+		require(false, stub_error);
+		tokenId;
+		key;
+		dummy;
+		return hex"";
+	}
+}
+
+// Selector: 42966c68
+contract ERC721Burnable is Dummy, ERC165 {
+	// @notice Burns a specific ERC721 token.
+	// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized
+	//  operator of the current owner.
+	// @param tokenId The RFT to approve
+	//
+	// Selector: burn(uint256) 42966c68
+	function burn(uint256 tokenId) public {
+		require(false, stub_error);
+		tokenId;
+		dummy = 0;
+	}
+}
+
+// Selector: 58800161
+contract ERC721 is Dummy, ERC165, ERC721Events {
+	// @notice Count all RFTs assigned to an owner
+	// @dev RFTs assigned to the zero address are considered invalid, and this
+	//  function throws for queries about the zero address.
+	// @param owner An address for whom to query the balance
+	// @return The number of RFTs owned by `owner`, possibly zero
+	//
+	// Selector: balanceOf(address) 70a08231
+	function balanceOf(address owner) public view returns (uint256) {
+		require(false, stub_error);
+		owner;
+		dummy;
+		return 0;
+	}
+
+	// @notice Find the owner of an RFT
+	// @dev RFTs assigned to zero address are considered invalid, and queries
+	//  about them do throw.
+	//  Returns special 0xffffffffffffffffffffffffffffffffffffffff address for
+	//  the tokens that are partially owned.
+	// @param tokenId The identifier for an RFT
+	// @return The address of the owner of the RFT
+	//
+	// Selector: ownerOf(uint256) 6352211e
+	function ownerOf(uint256 tokenId) public view returns (address) {
+		require(false, stub_error);
+		tokenId;
+		dummy;
+		return 0x0000000000000000000000000000000000000000;
+	}
+
+	// @dev Not implemented
+	//
+	// Selector: safeTransferFromWithData(address,address,uint256,bytes) 60a11672
+	function safeTransferFromWithData(
+		address from,
+		address to,
+		uint256 tokenId,
+		bytes memory data
+	) public {
+		require(false, stub_error);
+		from;
+		to;
+		tokenId;
+		data;
+		dummy = 0;
+	}
+
+	// @dev Not implemented
+	//
+	// Selector: safeTransferFrom(address,address,uint256) 42842e0e
+	function safeTransferFrom(
+		address from,
+		address to,
+		uint256 tokenId
+	) public {
+		require(false, stub_error);
+		from;
+		to;
+		tokenId;
+		dummy = 0;
+	}
+
+	// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE
+	//  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE
+	//  THEY MAY BE PERMANENTLY LOST
+	// @dev Throws unless `msg.sender` is the current owner or an authorized
+	//  operator for this RFT. Throws if `from` is not the current owner. Throws
+	//  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.
+	//  Throws if RFT pieces have multiple owners.
+	// @param from The current owner of the NFT
+	// @param to The new owner
+	// @param tokenId The NFT to transfer
+	// @param _value Not used for an NFT
+	//
+	// Selector: transferFrom(address,address,uint256) 23b872dd
+	function transferFrom(
+		address from,
+		address to,
+		uint256 tokenId
+	) public {
+		require(false, stub_error);
+		from;
+		to;
+		tokenId;
+		dummy = 0;
+	}
+
+	// @dev Not implemented
+	//
+	// Selector: approve(address,uint256) 095ea7b3
+	function approve(address approved, uint256 tokenId) public {
+		require(false, stub_error);
+		approved;
+		tokenId;
+		dummy = 0;
+	}
+
+	// @dev Not implemented
+	//
+	// Selector: setApprovalForAll(address,bool) a22cb465
+	function setApprovalForAll(address operator, bool approved) public {
+		require(false, stub_error);
+		operator;
+		approved;
+		dummy = 0;
+	}
+
+	// @dev Not implemented
+	//
+	// Selector: getApproved(uint256) 081812fc
+	function getApproved(uint256 tokenId) public view returns (address) {
+		require(false, stub_error);
+		tokenId;
+		dummy;
+		return 0x0000000000000000000000000000000000000000;
+	}
+
+	// @dev Not implemented
+	//
+	// Selector: isApprovedForAll(address,address) e985e9c5
+	function isApprovedForAll(address owner, address operator)
+		public
+		view
+		returns (address)
+	{
+		require(false, stub_error);
+		owner;
+		operator;
+		dummy;
+		return 0x0000000000000000000000000000000000000000;
+	}
+}
+
+// Selector: 5b5e139f
+contract ERC721Metadata is Dummy, ERC165 {
+	// @notice A descriptive name for a collection of RFTs in this contract
+	//
+	// Selector: name() 06fdde03
+	function name() public view returns (string memory) {
+		require(false, stub_error);
+		dummy;
+		return "";
+	}
+
+	// @notice An abbreviated name for RFTs in this contract
+	//
+	// Selector: symbol() 95d89b41
+	function symbol() public view returns (string memory) {
+		require(false, stub_error);
+		dummy;
+		return "";
+	}
+
+	// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
+	//
+	// @dev If the token has a `url` property and it is not empty, it is returned.
+	//  Else If the collection does not have a property with key `schemaName` or its value is not equal to `ERC721Metadata`, it return an error `tokenURI not set`.
+	//  If the collection property `baseURI` is empty or absent, return "" (empty string)
+	//  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix
+	//  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).
+	//
+	// @return token's const_metadata
+	//
+	// Selector: tokenURI(uint256) c87b56dd
+	function tokenURI(uint256 tokenId) public view returns (string memory) {
+		require(false, stub_error);
+		tokenId;
+		dummy;
+		return "";
+	}
+}
+
+// Selector: 68ccfe89
+contract ERC721Mintable is Dummy, ERC165, ERC721MintableEvents {
+	// Selector: mintingFinished() 05d2035b
+	function mintingFinished() public view returns (bool) {
+		require(false, stub_error);
+		dummy;
+		return false;
+	}
+
+	// @notice Function to mint token.
+	// @dev `tokenId` should be obtained with `nextTokenId` method,
+	//  unlike standard, you can't specify it manually
+	// @param to The new owner
+	// @param tokenId ID of the minted RFT
+	//
+	// Selector: mint(address,uint256) 40c10f19
+	function mint(address to, uint256 tokenId) public returns (bool) {
+		require(false, stub_error);
+		to;
+		tokenId;
+		dummy = 0;
+		return false;
+	}
+
+	// @notice Function to mint token with the given tokenUri.
+	// @dev `tokenId` should be obtained with `nextTokenId` method,
+	//  unlike standard, you can't specify it manually
+	// @param to The new owner
+	// @param tokenId ID of the minted RFT
+	// @param tokenUri Token URI that would be stored in the RFT properties
+	//
+	// Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f
+	function mintWithTokenURI(
+		address to,
+		uint256 tokenId,
+		string memory tokenUri
+	) public returns (bool) {
+		require(false, stub_error);
+		to;
+		tokenId;
+		tokenUri;
+		dummy = 0;
+		return false;
+	}
+
+	// @dev Not implemented
+	//
+	// Selector: finishMinting() 7d64bcb4
+	function finishMinting() public returns (bool) {
+		require(false, stub_error);
+		dummy = 0;
+		return false;
+	}
+}
+
+// Selector: 780e9d63
+contract ERC721Enumerable is Dummy, ERC165 {
+	// @notice Enumerate valid RFTs
+	// @param index A counter less than `totalSupply()`
+	// @return The token identifier for the `index`th NFT,
+	//  (sort order not specified)
+	//
+	// Selector: tokenByIndex(uint256) 4f6ccce7
+	function tokenByIndex(uint256 index) public view returns (uint256) {
+		require(false, stub_error);
+		index;
+		dummy;
+		return 0;
+	}
+
+	// Not implemented
+	//
+	// Selector: tokenOfOwnerByIndex(address,uint256) 2f745c59
+	function tokenOfOwnerByIndex(address owner, uint256 index)
+		public
+		view
+		returns (uint256)
+	{
+		require(false, stub_error);
+		owner;
+		index;
+		dummy;
+		return 0;
+	}
+
+	// @notice Count RFTs tracked by this contract
+	// @return A count of valid RFTs tracked by this contract, where each one of
+	//  them has an assigned and queryable owner not equal to the zero address
+	//
+	// Selector: totalSupply() 18160ddd
+	function totalSupply() public view returns (uint256) {
+		require(false, stub_error);
+		dummy;
+		return 0;
+	}
+}
+
 // Selector: 7d9262e6
 contract Collection is Dummy, ERC165 {
 	// Set collection property.
@@ -248,4 +640,96 @@
 	}
 }
 
-contract UniqueRefungible is Dummy, ERC165, Collection {}
+// Selector: d74d154f
+contract ERC721UniqueExtensions is Dummy, ERC165 {
+	// @notice Transfer ownership of an RFT
+	// @dev Throws unless `msg.sender` is the current owner. Throws if `to`
+	//  is the zero address. Throws if `tokenId` is not a valid RFT.
+	//  Throws if RFT pieces have multiple owners.
+	// @param to The new owner
+	// @param tokenId The RFT to transfer
+	// @param _value Not used for an RFT
+	//
+	// Selector: transfer(address,uint256) a9059cbb
+	function transfer(address to, uint256 tokenId) public {
+		require(false, stub_error);
+		to;
+		tokenId;
+		dummy = 0;
+	}
+
+	// @notice Burns a specific ERC721 token.
+	// @dev Throws unless `msg.sender` is the current owner or an authorized
+	//  operator for this RFT. Throws if `from` is not the current owner. Throws
+	//  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.
+	//  Throws if RFT pieces have multiple owners.
+	// @param from The current owner of the RFT
+	// @param tokenId The RFT to transfer
+	// @param _value Not used for an RFT
+	//
+	// Selector: burnFrom(address,uint256) 79cc6790
+	function burnFrom(address from, uint256 tokenId) public {
+		require(false, stub_error);
+		from;
+		tokenId;
+		dummy = 0;
+	}
+
+	// @notice Returns next free RFT ID.
+	//
+	// Selector: nextTokenId() 75794a3c
+	function nextTokenId() public view returns (uint256) {
+		require(false, stub_error);
+		dummy;
+		return 0;
+	}
+
+	// @notice Function to mint multiple tokens.
+	// @dev `tokenIds` should be an array of consecutive numbers and first number
+	//  should be obtained with `nextTokenId` method
+	// @param to The new owner
+	// @param tokenIds IDs of the minted RFTs
+	//
+	// Selector: mintBulk(address,uint256[]) 44a9945e
+	function mintBulk(address to, uint256[] memory tokenIds)
+		public
+		returns (bool)
+	{
+		require(false, stub_error);
+		to;
+		tokenIds;
+		dummy = 0;
+		return false;
+	}
+
+	// @notice Function to mint multiple tokens with the given tokenUris.
+	// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive
+	//  numbers and first number should be obtained with `nextTokenId` method
+	// @param to The new owner
+	// @param tokens array of pairs of token ID and token URI for minted tokens
+	//
+	// Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006
+	function mintBulkWithTokenURI(address to, Tuple0[] memory tokens)
+		public
+		returns (bool)
+	{
+		require(false, stub_error);
+		to;
+		tokens;
+		dummy = 0;
+		return false;
+	}
+}
+
+contract UniqueRefungible is
+	Dummy,
+	ERC165,
+	ERC721,
+	ERC721Metadata,
+	ERC721Enumerable,
+	ERC721UniqueExtensions,
+	ERC721Mintable,
+	ERC721Burnable,
+	Collection,
+	TokenProperties
+{}
modifiedtests/src/eth/api/UniqueRefungible.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueRefungible.sol
+++ b/tests/src/eth/api/UniqueRefungible.sol
@@ -3,6 +3,12 @@
 
 pragma solidity >=0.8.0 <0.9.0;
 
+// Anonymous struct
+struct Tuple0 {
+	uint256 field_0;
+	string field_1;
+}
+
 // Common stubs holder
 interface Dummy {
 
@@ -12,6 +18,262 @@
 	function supportsInterface(bytes4 interfaceID) external view returns (bool);
 }
 
+// Inline
+interface ERC721Events {
+	event Transfer(
+		address indexed from,
+		address indexed to,
+		uint256 indexed tokenId
+	);
+	event Approval(
+		address indexed owner,
+		address indexed approved,
+		uint256 indexed tokenId
+	);
+	event ApprovalForAll(
+		address indexed owner,
+		address indexed operator,
+		bool approved
+	);
+}
+
+// Inline
+interface ERC721MintableEvents {
+	event MintingFinished();
+}
+
+// Selector: 41369377
+interface TokenProperties is Dummy, ERC165 {
+	// @notice Set permissions for token property.
+	// @dev Throws error if `msg.sender` is not admin or owner of the collection.
+	// @param key Property key.
+	// @param is_mutable Permission to mutate property.
+	// @param collection_admin Permission to mutate property by collection admin if property is mutable.
+	// @param token_owner Permission to mutate property by token owner if property is mutable.
+	//
+	// Selector: setTokenPropertyPermission(string,bool,bool,bool) 222d97fa
+	function setTokenPropertyPermission(
+		string memory key,
+		bool isMutable,
+		bool collectionAdmin,
+		bool tokenOwner
+	) external;
+
+	// @notice Set token property value.
+	// @dev Throws error if `msg.sender` has no permission to edit the property.
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	// @param value Property value.
+	//
+	// Selector: setProperty(uint256,string,bytes) 1752d67b
+	function setProperty(
+		uint256 tokenId,
+		string memory key,
+		bytes memory value
+	) external;
+
+	// @notice Delete token property value.
+	// @dev Throws error if `msg.sender` has no permission to edit the property.
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	//
+	// Selector: deleteProperty(uint256,string) 066111d1
+	function deleteProperty(uint256 tokenId, string memory key) external;
+
+	// @notice Get token property value.
+	// @dev Throws error if key not found
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	// @return Property value bytes
+	//
+	// Selector: property(uint256,string) 7228c327
+	function property(uint256 tokenId, string memory key)
+		external
+		view
+		returns (bytes memory);
+}
+
+// Selector: 42966c68
+interface ERC721Burnable is Dummy, ERC165 {
+	// @notice Burns a specific ERC721 token.
+	// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized
+	//  operator of the current owner.
+	// @param tokenId The RFT to approve
+	//
+	// Selector: burn(uint256) 42966c68
+	function burn(uint256 tokenId) external;
+}
+
+// Selector: 58800161
+interface ERC721 is Dummy, ERC165, ERC721Events {
+	// @notice Count all RFTs assigned to an owner
+	// @dev RFTs assigned to the zero address are considered invalid, and this
+	//  function throws for queries about the zero address.
+	// @param owner An address for whom to query the balance
+	// @return The number of RFTs owned by `owner`, possibly zero
+	//
+	// Selector: balanceOf(address) 70a08231
+	function balanceOf(address owner) external view returns (uint256);
+
+	// @notice Find the owner of an RFT
+	// @dev RFTs assigned to zero address are considered invalid, and queries
+	//  about them do throw.
+	//  Returns special 0xffffffffffffffffffffffffffffffffffffffff address for
+	//  the tokens that are partially owned.
+	// @param tokenId The identifier for an RFT
+	// @return The address of the owner of the RFT
+	//
+	// Selector: ownerOf(uint256) 6352211e
+	function ownerOf(uint256 tokenId) external view returns (address);
+
+	// @dev Not implemented
+	//
+	// Selector: safeTransferFromWithData(address,address,uint256,bytes) 60a11672
+	function safeTransferFromWithData(
+		address from,
+		address to,
+		uint256 tokenId,
+		bytes memory data
+	) external;
+
+	// @dev Not implemented
+	//
+	// Selector: safeTransferFrom(address,address,uint256) 42842e0e
+	function safeTransferFrom(
+		address from,
+		address to,
+		uint256 tokenId
+	) external;
+
+	// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE
+	//  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE
+	//  THEY MAY BE PERMANENTLY LOST
+	// @dev Throws unless `msg.sender` is the current owner or an authorized
+	//  operator for this RFT. Throws if `from` is not the current owner. Throws
+	//  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.
+	//  Throws if RFT pieces have multiple owners.
+	// @param from The current owner of the NFT
+	// @param to The new owner
+	// @param tokenId The NFT to transfer
+	// @param _value Not used for an NFT
+	//
+	// Selector: transferFrom(address,address,uint256) 23b872dd
+	function transferFrom(
+		address from,
+		address to,
+		uint256 tokenId
+	) external;
+
+	// @dev Not implemented
+	//
+	// Selector: approve(address,uint256) 095ea7b3
+	function approve(address approved, uint256 tokenId) external;
+
+	// @dev Not implemented
+	//
+	// Selector: setApprovalForAll(address,bool) a22cb465
+	function setApprovalForAll(address operator, bool approved) external;
+
+	// @dev Not implemented
+	//
+	// Selector: getApproved(uint256) 081812fc
+	function getApproved(uint256 tokenId) external view returns (address);
+
+	// @dev Not implemented
+	//
+	// Selector: isApprovedForAll(address,address) e985e9c5
+	function isApprovedForAll(address owner, address operator)
+		external
+		view
+		returns (address);
+}
+
+// Selector: 5b5e139f
+interface ERC721Metadata is Dummy, ERC165 {
+	// @notice A descriptive name for a collection of RFTs in this contract
+	//
+	// Selector: name() 06fdde03
+	function name() external view returns (string memory);
+
+	// @notice An abbreviated name for RFTs in this contract
+	//
+	// Selector: symbol() 95d89b41
+	function symbol() external view returns (string memory);
+
+	// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
+	//
+	// @dev If the token has a `url` property and it is not empty, it is returned.
+	//  Else If the collection does not have a property with key `schemaName` or its value is not equal to `ERC721Metadata`, it return an error `tokenURI not set`.
+	//  If the collection property `baseURI` is empty or absent, return "" (empty string)
+	//  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix
+	//  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).
+	//
+	// @return token's const_metadata
+	//
+	// Selector: tokenURI(uint256) c87b56dd
+	function tokenURI(uint256 tokenId) external view returns (string memory);
+}
+
+// Selector: 68ccfe89
+interface ERC721Mintable is Dummy, ERC165, ERC721MintableEvents {
+	// Selector: mintingFinished() 05d2035b
+	function mintingFinished() external view returns (bool);
+
+	// @notice Function to mint token.
+	// @dev `tokenId` should be obtained with `nextTokenId` method,
+	//  unlike standard, you can't specify it manually
+	// @param to The new owner
+	// @param tokenId ID of the minted RFT
+	//
+	// Selector: mint(address,uint256) 40c10f19
+	function mint(address to, uint256 tokenId) external returns (bool);
+
+	// @notice Function to mint token with the given tokenUri.
+	// @dev `tokenId` should be obtained with `nextTokenId` method,
+	//  unlike standard, you can't specify it manually
+	// @param to The new owner
+	// @param tokenId ID of the minted RFT
+	// @param tokenUri Token URI that would be stored in the RFT properties
+	//
+	// Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f
+	function mintWithTokenURI(
+		address to,
+		uint256 tokenId,
+		string memory tokenUri
+	) external returns (bool);
+
+	// @dev Not implemented
+	//
+	// Selector: finishMinting() 7d64bcb4
+	function finishMinting() external returns (bool);
+}
+
+// Selector: 780e9d63
+interface ERC721Enumerable is Dummy, ERC165 {
+	// @notice Enumerate valid RFTs
+	// @param index A counter less than `totalSupply()`
+	// @return The token identifier for the `index`th NFT,
+	//  (sort order not specified)
+	//
+	// Selector: tokenByIndex(uint256) 4f6ccce7
+	function tokenByIndex(uint256 index) external view returns (uint256);
+
+	// Not implemented
+	//
+	// Selector: tokenOfOwnerByIndex(address,uint256) 2f745c59
+	function tokenOfOwnerByIndex(address owner, uint256 index)
+		external
+		view
+		returns (uint256);
+
+	// @notice Count RFTs tracked by this contract
+	// @return A count of valid RFTs tracked by this contract, where each one of
+	//  them has an assigned and queryable owner not equal to the zero address
+	//
+	// Selector: totalSupply() 18160ddd
+	function totalSupply() external view returns (uint256);
+}
+
 // Selector: 7d9262e6
 interface Collection is Dummy, ERC165 {
 	// Set collection property.
@@ -160,4 +422,68 @@
 	function setCollectionMintMode(bool mode) external;
 }
 
-interface UniqueRefungible is Dummy, ERC165, Collection {}
+// Selector: d74d154f
+interface ERC721UniqueExtensions is Dummy, ERC165 {
+	// @notice Transfer ownership of an RFT
+	// @dev Throws unless `msg.sender` is the current owner. Throws if `to`
+	//  is the zero address. Throws if `tokenId` is not a valid RFT.
+	//  Throws if RFT pieces have multiple owners.
+	// @param to The new owner
+	// @param tokenId The RFT to transfer
+	// @param _value Not used for an RFT
+	//
+	// Selector: transfer(address,uint256) a9059cbb
+	function transfer(address to, uint256 tokenId) external;
+
+	// @notice Burns a specific ERC721 token.
+	// @dev Throws unless `msg.sender` is the current owner or an authorized
+	//  operator for this RFT. Throws if `from` is not the current owner. Throws
+	//  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.
+	//  Throws if RFT pieces have multiple owners.
+	// @param from The current owner of the RFT
+	// @param tokenId The RFT to transfer
+	// @param _value Not used for an RFT
+	//
+	// Selector: burnFrom(address,uint256) 79cc6790
+	function burnFrom(address from, uint256 tokenId) external;
+
+	// @notice Returns next free RFT ID.
+	//
+	// Selector: nextTokenId() 75794a3c
+	function nextTokenId() external view returns (uint256);
+
+	// @notice Function to mint multiple tokens.
+	// @dev `tokenIds` should be an array of consecutive numbers and first number
+	//  should be obtained with `nextTokenId` method
+	// @param to The new owner
+	// @param tokenIds IDs of the minted RFTs
+	//
+	// Selector: mintBulk(address,uint256[]) 44a9945e
+	function mintBulk(address to, uint256[] memory tokenIds)
+		external
+		returns (bool);
+
+	// @notice Function to mint multiple tokens with the given tokenUris.
+	// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive
+	//  numbers and first number should be obtained with `nextTokenId` method
+	// @param to The new owner
+	// @param tokens array of pairs of token ID and token URI for minted tokens
+	//
+	// Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006
+	function mintBulkWithTokenURI(address to, Tuple0[] memory tokens)
+		external
+		returns (bool);
+}
+
+interface UniqueRefungible is
+	Dummy,
+	ERC165,
+	ERC721,
+	ERC721Metadata,
+	ERC721Enumerable,
+	ERC721UniqueExtensions,
+	ERC721Mintable,
+	ERC721Burnable,
+	Collection,
+	TokenProperties
+{}
addedtests/src/eth/reFungible.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/reFungible.test.ts
@@ -0,0 +1,465 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+import {createCollectionExpectSuccess, UNIQUE} from '../util/helpers';
+import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, evmCollection, evmCollectionHelpers, GAS_ARGS, getCollectionAddressFromResult, itWeb3, normalizeEvents, recordEthFee, recordEvents, tokenIdToAddress} from './util/helpers';
+import reFungibleTokenAbi from './reFungibleTokenAbi.json';
+import {expect} from 'chai';
+
+describe('Refungible: Information getting', () => {
+  itWeb3('totalSupply', async ({api, web3, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+    const nextTokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, nextTokenId).send();
+    const totalSupply = await contract.methods.totalSupply().call();
+    expect(totalSupply).to.equal('1');
+  });
+
+  itWeb3('balanceOf', async ({api, web3, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    {
+      const nextTokenId = await contract.methods.nextTokenId().call();
+      await contract.methods.mint(caller, nextTokenId).send();
+    }
+    {
+      const nextTokenId = await contract.methods.nextTokenId().call();
+      await contract.methods.mint(caller, nextTokenId).send();
+    }
+    {
+      const nextTokenId = await contract.methods.nextTokenId().call();
+      await contract.methods.mint(caller, nextTokenId).send();
+    }
+
+    const balance = await contract.methods.balanceOf(caller).call();
+
+    expect(balance).to.equal('3');
+  });
+
+  itWeb3('ownerOf', async ({api, web3, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    const owner = await contract.methods.ownerOf(tokenId).call();
+
+    expect(owner).to.equal(caller);
+  });
+
+  itWeb3('ownerOf after burn', async ({api, web3, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const receiver = createEthAccount(web3);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    const tokenAddress = tokenIdToAddress(collectionId, tokenId);
+    const tokenContract = new web3.eth.Contract(reFungibleTokenAbi as any, tokenAddress, {from: caller, ...GAS_ARGS});
+
+    await tokenContract.methods.repartition(2).send();
+    await tokenContract.methods.transfer(receiver, 1).send();
+
+    await tokenContract.methods.burnFrom(caller, 1).send();
+
+    const owner = await contract.methods.ownerOf(tokenId).call();
+
+    expect(owner).to.equal(receiver);
+  });
+
+  itWeb3('ownerOf for partial ownership', async ({api, web3, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const receiver = createEthAccount(web3);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    const tokenAddress = tokenIdToAddress(collectionId, tokenId);
+    const tokenContract = new web3.eth.Contract(reFungibleTokenAbi as any, tokenAddress, {from: caller, ...GAS_ARGS});
+
+    await tokenContract.methods.repartition(2).send();
+    await tokenContract.methods.transfer(receiver, 1).send();
+
+    const owner = await contract.methods.ownerOf(tokenId).call();
+
+    expect(owner).to.equal('0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF');
+  });
+});
+
+describe('Refungible: Plain calls', () => {
+  itWeb3('Can perform mint()', async ({web3, api, privateKeyWrapper}) => {
+    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, owner);
+    let result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+    const receiver = createEthAccount(web3);
+    const contract = evmCollection(web3, owner, collectionIdAddress, {type: 'ReFungible'});
+    const nextTokenId = await contract.methods.nextTokenId().call();
+
+    expect(nextTokenId).to.be.equal('1');
+    result = await contract.methods.mintWithTokenURI(
+      receiver,
+      nextTokenId,
+      'Test URI',
+    ).send();
+
+    const events = normalizeEvents(result.events);
+
+    expect(events).to.include.deep.members([
+      {
+        address: collectionIdAddress,
+        event: 'Transfer',
+        args: {
+          from: '0x0000000000000000000000000000000000000000',
+          to: receiver,
+          tokenId: nextTokenId,
+        },
+      },
+    ]);
+
+    expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI');
+  });
+
+  itWeb3('Can perform mintBulk()', async ({web3, api, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const receiver = createEthAccount(web3);
+
+    {
+      const nextTokenId = await contract.methods.nextTokenId().call();
+      expect(nextTokenId).to.be.equal('1');
+      const result = await contract.methods.mintBulkWithTokenURI(
+        receiver,
+        [
+          [nextTokenId, 'Test URI 0'],
+          [+nextTokenId + 1, 'Test URI 1'],
+          [+nextTokenId + 2, 'Test URI 2'],
+        ],
+      ).send();
+      const events = normalizeEvents(result.events);
+
+      expect(events).to.include.deep.members([
+        {
+          address: collectionIdAddress,
+          event: 'Transfer',
+          args: {
+            from: '0x0000000000000000000000000000000000000000',
+            to: receiver,
+            tokenId: nextTokenId,
+          },
+        },
+        {
+          address: collectionIdAddress,
+          event: 'Transfer',
+          args: {
+            from: '0x0000000000000000000000000000000000000000',
+            to: receiver,
+            tokenId: String(+nextTokenId + 1),
+          },
+        },
+        {
+          address: collectionIdAddress,
+          event: 'Transfer',
+          args: {
+            from: '0x0000000000000000000000000000000000000000',
+            to: receiver,
+            tokenId: String(+nextTokenId + 2),
+          },
+        },
+      ]);
+
+      expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI 0');
+      expect(await contract.methods.tokenURI(+nextTokenId + 1).call()).to.be.equal('Test URI 1');
+      expect(await contract.methods.tokenURI(+nextTokenId + 2).call()).to.be.equal('Test URI 2');
+    }
+  });
+
+  itWeb3('Can perform burn()', async ({web3, api, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+    {
+      const result = await contract.methods.burn(tokenId).send();
+      const events = normalizeEvents(result.events);
+      expect(events).to.include.deep.members([
+        {
+          address: collectionIdAddress,
+          event: 'Transfer',
+          args: {
+            from: caller,
+            to: '0x0000000000000000000000000000000000000000',
+            tokenId: tokenId.toString(),
+          },
+        },
+      ]);
+    }
+  });
+
+  itWeb3('Can perform transferFrom()', async ({web3, api, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const receiver = createEthAccount(web3);
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const tokenContract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: caller, ...GAS_ARGS});
+    await tokenContract.methods.repartition(15).send();
+
+    {
+      const erc20Events = await recordEvents(tokenContract, async () => {
+        const result = await contract.methods.transferFrom(caller, receiver, tokenId).send();
+        const events = normalizeEvents(result.events);
+        expect(events).to.include.deep.members([
+          {
+            address: collectionIdAddress,
+            event: 'Transfer',
+            args: {
+              from: caller,
+              to: receiver,
+              tokenId: tokenId.toString(),
+            },
+          },
+        ]);
+      });
+      
+      expect(erc20Events).to.include.deep.members([
+        {
+          address,
+          event: 'Transfer',
+          args: {
+            from: caller,
+            to: receiver,
+            value: '15',
+          },
+        },
+      ]);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(receiver).call();
+      expect(+balance).to.equal(1);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(caller).call();
+      expect(+balance).to.equal(0);
+    }
+  });
+
+  itWeb3('Can perform transfer()', async ({web3, api, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const receiver = createEthAccount(web3);
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    {
+      const result = await contract.methods.transfer(receiver, tokenId).send();
+      const events = normalizeEvents(result.events);
+      expect(events).to.include.deep.members([
+        {
+          address: collectionIdAddress,
+          event: 'Transfer',
+          args: {
+            from: caller,
+            to: receiver,
+            tokenId: tokenId.toString(),
+          },
+        },
+      ]);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(caller).call();
+      expect(+balance).to.equal(0);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(receiver).call();
+      expect(+balance).to.equal(1);
+    }
+  });
+
+  itWeb3('transfer event on transfer from partial ownership to full ownership', async ({api, web3, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const receiver = createEthAccount(web3);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    const tokenAddress = tokenIdToAddress(collectionId, tokenId);
+    const tokenContract = new web3.eth.Contract(reFungibleTokenAbi as any, tokenAddress, {from: caller, ...GAS_ARGS});
+
+    await tokenContract.methods.repartition(2).send();
+    await tokenContract.methods.transfer(receiver, 1).send();
+
+    const events =  await recordEvents(contract, async () => 
+      await tokenContract.methods.transfer(receiver, 1).send());
+    expect(events).to.deep.equal([
+      {
+        address: collectionIdAddress,
+        event: 'Transfer',
+        args: {
+          from: '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF',
+          to: receiver,
+          tokenId: tokenId.toString(),
+        },
+      },
+    ]);
+  });
+
+  itWeb3('transfer event on transfer from full ownership to partial ownership', async ({api, web3, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const receiver = createEthAccount(web3);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    const tokenAddress = tokenIdToAddress(collectionId, tokenId);
+    const tokenContract = new web3.eth.Contract(reFungibleTokenAbi as any, tokenAddress, {from: caller, ...GAS_ARGS});
+
+    await tokenContract.methods.repartition(2).send();
+    
+    const events =  await recordEvents(contract, async () => 
+      await tokenContract.methods.transfer(receiver, 1).send());
+      
+    expect(events).to.deep.equal([
+      {
+        address: collectionIdAddress,
+        event: 'Transfer',
+        args: {
+          from: caller,
+          to: '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF',
+          tokenId: tokenId.toString(),
+        },
+      },
+    ]);
+  });
+});
+
+describe('RFT: Fees', () => {
+  itWeb3('transferFrom() call fee is less than 0.2UNQ', async ({web3, api, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const receiver = createEthAccount(web3);
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    const cost = await recordEthFee(api, caller, () => contract.methods.transferFrom(caller, receiver, tokenId).send());
+    expect(cost < BigInt(0.2 * Number(UNIQUE)));
+    expect(cost > 0n);
+  });
+
+  itWeb3('transfer() call fee is less than 0.2UNQ', async ({web3, api, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const receiver = createEthAccount(web3);
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    const cost = await recordEthFee(api, caller, () => contract.methods.transfer(receiver, tokenId).send());
+    expect(cost < BigInt(0.2 * Number(UNIQUE)));
+    expect(cost > 0n);
+  });
+});
+
+describe('Common metadata', () => {
+  itWeb3('Returns collection name', async ({api, web3, privateKeyWrapper}) => {
+    const collection = await createCollectionExpectSuccess({
+      name: 'token name',
+      mode: {type: 'ReFungible'},
+    });
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+    const address = collectionIdToAddress(collection);
+    const contract = evmCollection(web3, caller, address, {type: 'ReFungible'});
+    const name = await contract.methods.name().call();
+
+    expect(name).to.equal('token name');
+  });
+
+  itWeb3('Returns symbol name', async ({api, web3, privateKeyWrapper}) => {
+    const collection = await createCollectionExpectSuccess({
+      tokenPrefix: 'TOK',
+      mode: {type: 'ReFungible'},
+    });
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+    const address = collectionIdToAddress(collection);
+    const contract = evmCollection(web3, caller, address, {type: 'ReFungible'});
+    const symbol = await contract.methods.symbol().call();
+
+    expect(symbol).to.equal('TOK');
+  });
+});
\ No newline at end of file
addedtests/src/eth/reFungibleAbi.jsondiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/reFungibleAbi.json
@@ -0,0 +1,530 @@
+[
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "owner",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "approved",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "uint256",
+        "name": "tokenId",
+        "type": "uint256"
+      }
+    ],
+    "name": "Approval",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "owner",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "operator",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "bool",
+        "name": "approved",
+        "type": "bool"
+      }
+    ],
+    "name": "ApprovalForAll",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [],
+    "name": "MintingFinished",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "from",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "to",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "uint256",
+        "name": "tokenId",
+        "type": "uint256"
+      }
+    ],
+    "name": "Transfer",
+    "type": "event"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "newAdmin", "type": "address" }
+    ],
+    "name": "addCollectionAdmin",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "newAdmin", "type": "uint256" }
+    ],
+    "name": "addCollectionAdminSubstrate",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "user", "type": "address" }
+    ],
+    "name": "addToCollectionAllowList",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "approved", "type": "address" },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "approve",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "owner", "type": "address" }
+    ],
+    "name": "balanceOf",
+    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "burn",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "from", "type": "address" },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "burnFrom",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [{ "internalType": "string", "name": "key", "type": "string" }],
+    "name": "collectionProperty",
+    "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "confirmCollectionSponsorship",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "contractAddress",
+    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [{ "internalType": "string", "name": "key", "type": "string" }],
+    "name": "deleteCollectionProperty",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
+      { "internalType": "string", "name": "key", "type": "string" }
+    ],
+    "name": "deleteProperty",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "finishMinting",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "getApproved",
+    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "owner", "type": "address" },
+      { "internalType": "address", "name": "operator", "type": "address" }
+    ],
+    "name": "isApprovedForAll",
+    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "mint",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256[]", "name": "tokenIds", "type": "uint256[]" }
+    ],
+    "name": "mintBulk",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "to", "type": "address" },
+      {
+        "components": [
+          { "internalType": "uint256", "name": "field_0", "type": "uint256" },
+          { "internalType": "string", "name": "field_1", "type": "string" }
+        ],
+        "internalType": "struct Tuple0[]",
+        "name": "tokens",
+        "type": "tuple[]"
+      }
+    ],
+    "name": "mintBulkWithTokenURI",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
+      { "internalType": "string", "name": "tokenUri", "type": "string" }
+    ],
+    "name": "mintWithTokenURI",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "mintingFinished",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "name",
+    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "nextTokenId",
+    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "ownerOf",
+    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
+      { "internalType": "string", "name": "key", "type": "string" }
+    ],
+    "name": "property",
+    "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "admin", "type": "address" }
+    ],
+    "name": "removeCollectionAdmin",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "admin", "type": "uint256" }
+    ],
+    "name": "removeCollectionAdminSubstrate",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "user", "type": "address" }
+    ],
+    "name": "removeFromCollectionAllowList",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "from", "type": "address" },
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "safeTransferFrom",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "from", "type": "address" },
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
+      { "internalType": "bytes", "name": "data", "type": "bytes" }
+    ],
+    "name": "safeTransferFromWithData",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "operator", "type": "address" },
+      { "internalType": "bool", "name": "approved", "type": "bool" }
+    ],
+    "name": "setApprovalForAll",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [{ "internalType": "uint8", "name": "mode", "type": "uint8" }],
+    "name": "setCollectionAccess",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "string", "name": "limit", "type": "string" },
+      { "internalType": "uint32", "name": "value", "type": "uint32" }
+    ],
+    "name": "setCollectionLimit",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "string", "name": "limit", "type": "string" },
+      { "internalType": "bool", "name": "value", "type": "bool" }
+    ],
+    "name": "setCollectionLimit",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [{ "internalType": "bool", "name": "mode", "type": "bool" }],
+    "name": "setCollectionMintMode",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [{ "internalType": "bool", "name": "enable", "type": "bool" }],
+    "name": "setCollectionNesting",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "bool", "name": "enable", "type": "bool" },
+      {
+        "internalType": "address[]",
+        "name": "collections",
+        "type": "address[]"
+      }
+    ],
+    "name": "setCollectionNesting",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "string", "name": "key", "type": "string" },
+      { "internalType": "bytes", "name": "value", "type": "bytes" }
+    ],
+    "name": "setCollectionProperty",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "sponsor", "type": "address" }
+    ],
+    "name": "setCollectionSponsor",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
+      { "internalType": "string", "name": "key", "type": "string" },
+      { "internalType": "bytes", "name": "value", "type": "bytes" }
+    ],
+    "name": "setProperty",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "string", "name": "key", "type": "string" },
+      { "internalType": "bool", "name": "isMutable", "type": "bool" },
+      { "internalType": "bool", "name": "collectionAdmin", "type": "bool" },
+      { "internalType": "bool", "name": "tokenOwner", "type": "bool" }
+    ],
+    "name": "setTokenPropertyPermission",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" }
+    ],
+    "name": "supportsInterface",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "symbol",
+    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "index", "type": "uint256" }
+    ],
+    "name": "tokenByIndex",
+    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "owner", "type": "address" },
+      { "internalType": "uint256", "name": "index", "type": "uint256" }
+    ],
+    "name": "tokenOfOwnerByIndex",
+    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "tokenURI",
+    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "totalSupply",
+    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "transfer",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "from", "type": "address" },
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "transferFrom",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  }
+]
modifiedtests/src/eth/reFungibleToken.test.tsdiffbeforeafterboth
--- a/tests/src/eth/reFungibleToken.test.ts
+++ b/tests/src/eth/reFungibleToken.test.ts
@@ -74,7 +74,7 @@
 });
 
 // FIXME: Need erc721 for ReFubgible.
-describe.skip('Check ERC721 token URI for ReFungible', () => {
+describe('Check ERC721 token URI for ReFungible', () => {
   itWeb3('Empty tokenURI', async ({web3, api, privateKeyWrapper}) => {
     const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
     const helper = evmCollectionHelpers(web3, owner);
@@ -277,7 +277,7 @@
     {
       const result = await contract.methods.transferFrom(owner, receiver, 49).send({from: spender});
       const events = normalizeEvents(result.events);
-      expect(events).to.be.deep.equal([
+      expect(events).to.include.deep.members([
         {
           address,
           event: 'Transfer',
@@ -329,7 +329,7 @@
     {
       const result = await contract.methods.transfer(receiver, 50).send({from: owner});
       const events = normalizeEvents(result.events);
-      expect(events).to.be.deep.equal([
+      expect(events).to.include.deep.members([
         {
           address,
           event: 'Transfer',
@@ -442,6 +442,38 @@
       },
     ]);
   });
+
+  itWeb3('Receiving Transfer event on burning into full ownership', async ({web3, api, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const receiver = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+
+    const tokenContract =  new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: caller, ...GAS_ARGS});
+    await tokenContract.methods.repartition(2).send();
+    await tokenContract.methods.transfer(receiver, 1).send();
+
+    const events =  await recordEvents(contract, async () => 
+      await tokenContract.methods.burnFrom(caller, 1).send());
+    expect(events).to.deep.equal([
+      {
+        address: collectionIdAddress,
+        event: 'Transfer',
+        args: {
+          from: '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF',
+          to: receiver,
+          tokenId,
+        },
+      },
+    ]);
+  });
 });
 
 describe('Refungible: Fees', () => {
modifiedtests/src/eth/util/helpers.tsdiffbeforeafterboth
--- a/tests/src/eth/util/helpers.ts
+++ b/tests/src/eth/util/helpers.ts
@@ -31,7 +31,7 @@
 import collectionHelpersAbi from '../collectionHelpersAbi.json';
 import fungibleAbi from '../fungibleAbi.json';
 import nonFungibleAbi from '../nonFungibleAbi.json';
-import refungibleAbi from '../refungibleAbi.json';
+import refungibleAbi from '../reFungibleAbi.json';
 import contractHelpersAbi from './contractHelpersAbi.json';
 
 export const GAS_ARGS = {gas: 2500000};