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

difftreelog

chore implement transfer and burn for ERC-721

Grigoriy Simonov2022-07-28parent: #7d1859c.patch.diff
in: master

9 files changed

modifiedMakefilediffbeforeafterboth
--- a/Makefile
+++ b/Makefile
@@ -41,10 +41,6 @@
 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
-	
-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
 
 UniqueRefungibleToken.sol:
 	PACKAGE=pallet-refungible NAME=erc_token::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
@@ -73,10 +69,6 @@
 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
-
-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
 
 ContractHelpers: ContractHelpers.sol
 	INPUT=$(CONTRACT_HELPERS_STUBS)/$< OUTPUT=$(CONTRACT_HELPERS_STUBS)/ContractHelpers.raw ./.maintain/scripts/compile_stub.sh
modifiedpallets/refungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -34,8 +34,9 @@
 		CommonEvmHandler, CollectionCall,
 		static_property::{key, value as property_value},
 	},
+	eth::collection_id_to_address,
 };
-use pallet_evm::{account::CrossAccountId, PrecompileHandle};
+use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm, PrecompileHandle};
 use pallet_evm_coder_substrate::{call, dispatch_to_evm};
 use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};
 use sp_core::H160;
@@ -46,8 +47,8 @@
 };
 
 use crate::{
-	AccountBalance, Config, CreateItemData, Pallet, RefungibleHandle, SelfWeightOf,
-	TokenProperties, TokensMinted, weights::WeightInfo,
+	AccountBalance, Balance, Config, CreateItemData, Pallet, RefungibleHandle, SelfWeightOf,
+	TokenProperties, TokensMinted, TotalSupply, weights::WeightInfo,
 };
 
 /// @title A contract that allows to set and delete token properties and change token property permissions.
@@ -331,16 +332,49 @@
 		Err("not implemented".into())
 	}
 
-	/// @dev Not implemented
+	/// @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,
+		caller: caller,
+		from: address,
+		to: address,
+		token_id: uint256,
 		_value: value,
 	) -> Result<void> {
-		Err("not implemented".into())
+		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>)?;
+
+		<PalletEvm<T>>::deposit_log(
+			ERC721Events::Transfer {
+				from: *from.as_eth(),
+				to: *to.as_eth(),
+				token_id: token_id.into(),
+			}
+			.to_log(collection_id_to_address(self.id)),
+		);
+		Ok(())
 	}
 
 	/// @dev Not implemented
@@ -378,12 +412,48 @@
 	}
 }
 
+/// 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> {
-	/// @dev Not implemented
-	fn burn(&mut self, _caller: caller, _token_id: uint256, _value: value) -> Result<void> {
-		Err("not implemented".into())
+	/// @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(())
 	}
 }
 
@@ -555,6 +625,75 @@
 /// @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>)?;
+		<PalletEvm<T>>::deposit_log(
+			ERC721Events::Transfer {
+				from: *caller.as_eth(),
+				to: *to.as_eth(),
+				token_id: token_id.into(),
+			}
+			.to_log(collection_id_to_address(self.id)),
+		);
+		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)?;
modifiedpallets/refungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -454,6 +454,14 @@
 			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);
 			<AccountBalance<T>>::insert((collection.id, owner), account_balance);
 			Self::burn_token_unchecked(collection, token)?;
+			<PalletEvm<T>>::deposit_log(
+				ERC721Events::Transfer {
+					from: *owner.as_eth(),
+					to: H160::default(),
+					token_id: token.into(),
+				}
+				.to_log(collection_id_to_address(collection.id)),
+			);
 			<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(
 				collection.id,
 				token,
modifiedpallets/refungible/src/stubs/UniqueRefungible.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/refungible/src/stubs/UniqueRefungible.soldiffbeforeafterboth
after · pallets/refungible/src/stubs/UniqueRefungible.sol
1// SPDX-License-Identifier: OTHER2// This code is automatically generated34pragma solidity >=0.8.0 <0.9.0;56// Anonymous struct7struct Tuple0 {8	uint256 field_0;9	string field_1;10}1112// Common stubs holder13contract Dummy {14	uint8 dummy;15	string stub_error = "this contract is implemented in native";16}1718contract ERC165 is Dummy {19	function supportsInterface(bytes4 interfaceID)20		external21		view22		returns (bool)23	{24		require(false, stub_error);25		interfaceID;26		return true;27	}28}2930// Inline31contract ERC721Events {32	event Transfer(33		address indexed from,34		address indexed to,35		uint256 indexed tokenId36	);37	event Approval(38		address indexed owner,39		address indexed approved,40		uint256 indexed tokenId41	);42	event ApprovalForAll(43		address indexed owner,44		address indexed operator,45		bool approved46	);47}4849// Inline50contract ERC721MintableEvents {51	event MintingFinished();52}5354// Selector: 4136937755contract TokenProperties is Dummy, ERC165 {56	// @notice Set permissions for token property.57	// @dev Throws error if `msg.sender` is not admin or owner of the collection.58	// @param key Property key.59	// @param is_mutable Permission to mutate property.60	// @param collection_admin Permission to mutate property by collection admin if property is mutable.61	// @param token_owner Permission to mutate property by token owner if property is mutable.62	//63	// Selector: setTokenPropertyPermission(string,bool,bool,bool) 222d97fa64	function setTokenPropertyPermission(65		string memory key,66		bool isMutable,67		bool collectionAdmin,68		bool tokenOwner69	) public {70		require(false, stub_error);71		key;72		isMutable;73		collectionAdmin;74		tokenOwner;75		dummy = 0;76	}7778	// @notice Set token property value.79	// @dev Throws error if `msg.sender` has no permission to edit the property.80	// @param tokenId ID of the token.81	// @param key Property key.82	// @param value Property value.83	//84	// Selector: setProperty(uint256,string,bytes) 1752d67b85	function setProperty(86		uint256 tokenId,87		string memory key,88		bytes memory value89	) public {90		require(false, stub_error);91		tokenId;92		key;93		value;94		dummy = 0;95	}9697	// @notice Delete token property value.98	// @dev Throws error if `msg.sender` has no permission to edit the property.99	// @param tokenId ID of the token.100	// @param key Property key.101	//102	// Selector: deleteProperty(uint256,string) 066111d1103	function deleteProperty(uint256 tokenId, string memory key) public {104		require(false, stub_error);105		tokenId;106		key;107		dummy = 0;108	}109110	// @notice Get token property value.111	// @dev Throws error if key not found112	// @param tokenId ID of the token.113	// @param key Property key.114	// @return Property value bytes115	//116	// Selector: property(uint256,string) 7228c327117	function property(uint256 tokenId, string memory key)118		public119		view120		returns (bytes memory)121	{122		require(false, stub_error);123		tokenId;124		key;125		dummy;126		return hex"";127	}128}129130// Selector: 42966c68131contract ERC721Burnable is Dummy, ERC165 {132	// @notice Burns a specific ERC721 token.133	// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized134	//  operator of the current owner.135	// @param tokenId The RFT to approve136	//137	// Selector: burn(uint256) 42966c68138	function burn(uint256 tokenId) public {139		require(false, stub_error);140		tokenId;141		dummy = 0;142	}143}144145// Selector: 58800161146contract ERC721 is Dummy, ERC165, ERC721Events {147	// @notice Count all RFTs assigned to an owner148	// @dev RFTs assigned to the zero address are considered invalid, and this149	//  function throws for queries about the zero address.150	// @param owner An address for whom to query the balance151	// @return The number of RFTs owned by `owner`, possibly zero152	//153	// Selector: balanceOf(address) 70a08231154	function balanceOf(address owner) public view returns (uint256) {155		require(false, stub_error);156		owner;157		dummy;158		return 0;159	}160161	// Selector: ownerOf(uint256) 6352211e162	function ownerOf(uint256 tokenId) public view returns (address) {163		require(false, stub_error);164		tokenId;165		dummy;166		return 0x0000000000000000000000000000000000000000;167	}168169	// @dev Not implemented170	//171	// Selector: safeTransferFromWithData(address,address,uint256,bytes) 60a11672172	function safeTransferFromWithData(173		address from,174		address to,175		uint256 tokenId,176		bytes memory data177	) public {178		require(false, stub_error);179		from;180		to;181		tokenId;182		data;183		dummy = 0;184	}185186	// @dev Not implemented187	//188	// Selector: safeTransferFrom(address,address,uint256) 42842e0e189	function safeTransferFrom(190		address from,191		address to,192		uint256 tokenId193	) public {194		require(false, stub_error);195		from;196		to;197		tokenId;198		dummy = 0;199	}200201	// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE202	//  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE203	//  THEY MAY BE PERMANENTLY LOST204	// @dev Throws unless `msg.sender` is the current owner or an authorized205	//  operator for this RFT. Throws if `from` is not the current owner. Throws206	//  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.207	//  Throws if RFT pieces have multiple owners.208	// @param from The current owner of the NFT209	// @param to The new owner210	// @param tokenId The NFT to transfer211	// @param _value Not used for an NFT212	//213	// Selector: transferFrom(address,address,uint256) 23b872dd214	function transferFrom(215		address from,216		address to,217		uint256 tokenId218	) public {219		require(false, stub_error);220		from;221		to;222		tokenId;223		dummy = 0;224	}225226	// @dev Not implemented227	//228	// Selector: approve(address,uint256) 095ea7b3229	function approve(address approved, uint256 tokenId) public {230		require(false, stub_error);231		approved;232		tokenId;233		dummy = 0;234	}235236	// @dev Not implemented237	//238	// Selector: setApprovalForAll(address,bool) a22cb465239	function setApprovalForAll(address operator, bool approved) public {240		require(false, stub_error);241		operator;242		approved;243		dummy = 0;244	}245246	// @dev Not implemented247	//248	// Selector: getApproved(uint256) 081812fc249	function getApproved(uint256 tokenId) public view returns (address) {250		require(false, stub_error);251		tokenId;252		dummy;253		return 0x0000000000000000000000000000000000000000;254	}255256	// @dev Not implemented257	//258	// Selector: isApprovedForAll(address,address) e985e9c5259	function isApprovedForAll(address owner, address operator)260		public261		view262		returns (address)263	{264		require(false, stub_error);265		owner;266		operator;267		dummy;268		return 0x0000000000000000000000000000000000000000;269	}270}271272// Selector: 5b5e139f273contract ERC721Metadata is Dummy, ERC165 {274	// @notice A descriptive name for a collection of RFTs in this contract275	//276	// Selector: name() 06fdde03277	function name() public view returns (string memory) {278		require(false, stub_error);279		dummy;280		return "";281	}282283	// @notice An abbreviated name for RFTs in this contract284	//285	// Selector: symbol() 95d89b41286	function symbol() public view returns (string memory) {287		require(false, stub_error);288		dummy;289		return "";290	}291292	// @notice A distinct Uniform Resource Identifier (URI) for a given asset.293	//294	// @dev If the token has a `url` property and it is not empty, it is returned.295	//  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`.296	//  If the collection property `baseURI` is empty or absent, return "" (empty string)297	//  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix298	//  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).299	//300	// @return token's const_metadata301	//302	// Selector: tokenURI(uint256) c87b56dd303	function tokenURI(uint256 tokenId) public view returns (string memory) {304		require(false, stub_error);305		tokenId;306		dummy;307		return "";308	}309}310311// Selector: 68ccfe89312contract ERC721Mintable is Dummy, ERC165, ERC721MintableEvents {313	// Selector: mintingFinished() 05d2035b314	function mintingFinished() public view returns (bool) {315		require(false, stub_error);316		dummy;317		return false;318	}319320	// @notice Function to mint token.321	// @dev `tokenId` should be obtained with `nextTokenId` method,322	//  unlike standard, you can't specify it manually323	// @param to The new owner324	// @param tokenId ID of the minted RFT325	//326	// Selector: mint(address,uint256) 40c10f19327	function mint(address to, uint256 tokenId) public returns (bool) {328		require(false, stub_error);329		to;330		tokenId;331		dummy = 0;332		return false;333	}334335	// @notice Function to mint token with the given tokenUri.336	// @dev `tokenId` should be obtained with `nextTokenId` method,337	//  unlike standard, you can't specify it manually338	// @param to The new owner339	// @param tokenId ID of the minted RFT340	// @param tokenUri Token URI that would be stored in the RFT properties341	//342	// Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f343	function mintWithTokenURI(344		address to,345		uint256 tokenId,346		string memory tokenUri347	) public returns (bool) {348		require(false, stub_error);349		to;350		tokenId;351		tokenUri;352		dummy = 0;353		return false;354	}355356	// @dev Not implemented357	//358	// Selector: finishMinting() 7d64bcb4359	function finishMinting() public returns (bool) {360		require(false, stub_error);361		dummy = 0;362		return false;363	}364}365366// Selector: 780e9d63367contract ERC721Enumerable is Dummy, ERC165 {368	// @notice Enumerate valid RFTs369	// @param index A counter less than `totalSupply()`370	// @return The token identifier for the `index`th NFT,371	//  (sort order not specified)372	//373	// Selector: tokenByIndex(uint256) 4f6ccce7374	function tokenByIndex(uint256 index) public view returns (uint256) {375		require(false, stub_error);376		index;377		dummy;378		return 0;379	}380381	// Not implemented382	//383	// Selector: tokenOfOwnerByIndex(address,uint256) 2f745c59384	function tokenOfOwnerByIndex(address owner, uint256 index)385		public386		view387		returns (uint256)388	{389		require(false, stub_error);390		owner;391		index;392		dummy;393		return 0;394	}395396	// @notice Count RFTs tracked by this contract397	// @return A count of valid RFTs tracked by this contract, where each one of398	//  them has an assigned and queryable owner not equal to the zero address399	//400	// Selector: totalSupply() 18160ddd401	function totalSupply() public view returns (uint256) {402		require(false, stub_error);403		dummy;404		return 0;405	}406}407408// Selector: 7d9262e6409contract Collection is Dummy, ERC165 {410	// Set collection property.411	//412	// @param key Property key.413	// @param value Propery value.414	//415	// Selector: setCollectionProperty(string,bytes) 2f073f66416	function setCollectionProperty(string memory key, bytes memory value)417		public418	{419		require(false, stub_error);420		key;421		value;422		dummy = 0;423	}424425	// Delete collection property.426	//427	// @param key Property key.428	//429	// Selector: deleteCollectionProperty(string) 7b7debce430	function deleteCollectionProperty(string memory key) public {431		require(false, stub_error);432		key;433		dummy = 0;434	}435436	// Get collection property.437	//438	// @dev Throws error if key not found.439	//440	// @param key Property key.441	// @return bytes The property corresponding to the key.442	//443	// Selector: collectionProperty(string) cf24fd6d444	function collectionProperty(string memory key)445		public446		view447		returns (bytes memory)448	{449		require(false, stub_error);450		key;451		dummy;452		return hex"";453	}454455	// Set the sponsor of the collection.456	//457	// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.458	//459	// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.460	//461	// Selector: setCollectionSponsor(address) 7623402e462	function setCollectionSponsor(address sponsor) public {463		require(false, stub_error);464		sponsor;465		dummy = 0;466	}467468	// Collection sponsorship confirmation.469	//470	// @dev After setting the sponsor for the collection, it must be confirmed with this function.471	//472	// Selector: confirmCollectionSponsorship() 3c50e97a473	function confirmCollectionSponsorship() public {474		require(false, stub_error);475		dummy = 0;476	}477478	// Set limits for the collection.479	// @dev Throws error if limit not found.480	// @param limit Name of the limit. Valid names:481	// 	"accountTokenOwnershipLimit",482	// 	"sponsoredDataSize",483	// 	"sponsoredDataRateLimit",484	// 	"tokenLimit",485	// 	"sponsorTransferTimeout",486	// 	"sponsorApproveTimeout"487	// @param value Value of the limit.488	//489	// Selector: setCollectionLimit(string,uint32) 6a3841db490	function setCollectionLimit(string memory limit, uint32 value) public {491		require(false, stub_error);492		limit;493		value;494		dummy = 0;495	}496497	// Set limits for the collection.498	// @dev Throws error if limit not found.499	// @param limit Name of the limit. Valid names:500	// 	"ownerCanTransfer",501	// 	"ownerCanDestroy",502	// 	"transfersEnabled"503	// @param value Value of the limit.504	//505	// Selector: setCollectionLimit(string,bool) 993b7fba506	function setCollectionLimit(string memory limit, bool value) public {507		require(false, stub_error);508		limit;509		value;510		dummy = 0;511	}512513	// Get contract address.514	//515	// Selector: contractAddress() f6b4dfb4516	function contractAddress() public view returns (address) {517		require(false, stub_error);518		dummy;519		return 0x0000000000000000000000000000000000000000;520	}521522	// Add collection admin by substrate address.523	// @param new_admin Substrate administrator address.524	//525	// Selector: addCollectionAdminSubstrate(uint256) 5730062b526	function addCollectionAdminSubstrate(uint256 newAdmin) public {527		require(false, stub_error);528		newAdmin;529		dummy = 0;530	}531532	// Remove collection admin by substrate address.533	// @param admin Substrate administrator address.534	//535	// Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9536	function removeCollectionAdminSubstrate(uint256 admin) public {537		require(false, stub_error);538		admin;539		dummy = 0;540	}541542	// Add collection admin.543	// @param new_admin Address of the added administrator.544	//545	// Selector: addCollectionAdmin(address) 92e462c7546	function addCollectionAdmin(address newAdmin) public {547		require(false, stub_error);548		newAdmin;549		dummy = 0;550	}551552	// Remove collection admin.553	//554	// @param new_admin Address of the removed administrator.555	//556	// Selector: removeCollectionAdmin(address) fafd7b42557	function removeCollectionAdmin(address admin) public {558		require(false, stub_error);559		admin;560		dummy = 0;561	}562563	// Toggle accessibility of collection nesting.564	//565	// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'566	//567	// Selector: setCollectionNesting(bool) 112d4586568	function setCollectionNesting(bool enable) public {569		require(false, stub_error);570		enable;571		dummy = 0;572	}573574	// Toggle accessibility of collection nesting.575	//576	// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'577	// @param collections Addresses of collections that will be available for nesting.578	//579	// Selector: setCollectionNesting(bool,address[]) 64872396580	function setCollectionNesting(bool enable, address[] memory collections)581		public582	{583		require(false, stub_error);584		enable;585		collections;586		dummy = 0;587	}588589	// Set the collection access method.590	// @param mode Access mode591	// 	0 for Normal592	// 	1 for AllowList593	//594	// Selector: setCollectionAccess(uint8) 41835d4c595	function setCollectionAccess(uint8 mode) public {596		require(false, stub_error);597		mode;598		dummy = 0;599	}600601	// Add the user to the allowed list.602	//603	// @param user Address of a trusted user.604	//605	// Selector: addToCollectionAllowList(address) 67844fe6606	function addToCollectionAllowList(address user) public {607		require(false, stub_error);608		user;609		dummy = 0;610	}611612	// Remove the user from the allowed list.613	//614	// @param user Address of a removed user.615	//616	// Selector: removeFromCollectionAllowList(address) 85c51acb617	function removeFromCollectionAllowList(address user) public {618		require(false, stub_error);619		user;620		dummy = 0;621	}622623	// Switch permission for minting.624	//625	// @param mode Enable if "true".626	//627	// Selector: setCollectionMintMode(bool) 00018e84628	function setCollectionMintMode(bool mode) public {629		require(false, stub_error);630		mode;631		dummy = 0;632	}633}634635// Selector: d74d154f636contract ERC721UniqueExtensions is Dummy, ERC165 {637	// @notice Transfer ownership of an RFT638	// @dev Throws unless `msg.sender` is the current owner. Throws if `to`639	//  is the zero address. Throws if `tokenId` is not a valid RFT.640	//  Throws if RFT pieces have multiple owners.641	// @param to The new owner642	// @param tokenId The RFT to transfer643	// @param _value Not used for an RFT644	//645	// Selector: transfer(address,uint256) a9059cbb646	function transfer(address to, uint256 tokenId) public {647		require(false, stub_error);648		to;649		tokenId;650		dummy = 0;651	}652653	// @notice Burns a specific ERC721 token.654	// @dev Throws unless `msg.sender` is the current owner or an authorized655	//  operator for this RFT. Throws if `from` is not the current owner. Throws656	//  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.657	//  Throws if RFT pieces have multiple owners.658	// @param from The current owner of the RFT659	// @param tokenId The RFT to transfer660	// @param _value Not used for an RFT661	//662	// Selector: burnFrom(address,uint256) 79cc6790663	function burnFrom(address from, uint256 tokenId) public {664		require(false, stub_error);665		from;666		tokenId;667		dummy = 0;668	}669670	// @notice Returns next free RFT ID.671	//672	// Selector: nextTokenId() 75794a3c673	function nextTokenId() public view returns (uint256) {674		require(false, stub_error);675		dummy;676		return 0;677	}678679	// @notice Function to mint multiple tokens.680	// @dev `tokenIds` should be an array of consecutive numbers and first number681	//  should be obtained with `nextTokenId` method682	// @param to The new owner683	// @param tokenIds IDs of the minted RFTs684	//685	// Selector: mintBulk(address,uint256[]) 44a9945e686	function mintBulk(address to, uint256[] memory tokenIds)687		public688		returns (bool)689	{690		require(false, stub_error);691		to;692		tokenIds;693		dummy = 0;694		return false;695	}696697	// @notice Function to mint multiple tokens with the given tokenUris.698	// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive699	//  numbers and first number should be obtained with `nextTokenId` method700	// @param to The new owner701	// @param tokens array of pairs of token ID and token URI for minted tokens702	//703	// Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006704	function mintBulkWithTokenURI(address to, Tuple0[] memory tokens)705		public706		returns (bool)707	{708		require(false, stub_error);709		to;710		tokens;711		dummy = 0;712		return false;713	}714}715716contract UniqueRefungible is717	Dummy,718	ERC165,719	ERC721,720	ERC721Metadata,721	ERC721Enumerable,722	ERC721UniqueExtensions,723	ERC721Mintable,724	ERC721Burnable,725	Collection,726	TokenProperties727{}
modifiedpallets/refungible/src/stubs/UniqueRefungibleToken.rawdiffbeforeafterboth

binary blob — no preview

modifiedtests/src/eth/api/UniqueRefungible.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueRefungible.sol
+++ b/tests/src/eth/api/UniqueRefungible.sol
@@ -42,26 +42,15 @@
 	event MintingFinished();
 }
 
-// Selector: 0784ee64
-interface ERC721UniqueExtensions is Dummy, ERC165 {
-	// @notice Returns next free RFT ID.
-	//
-	// Selector: nextTokenId() 75794a3c
-	function nextTokenId() external view returns (uint256);
-
-	// Selector: mintBulk(address,uint256[]) 44a9945e
-	function mintBulk(address to, uint256[] memory tokenIds)
-		external
-		returns (bool);
-
-	// Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006
-	function mintBulkWithTokenURI(address to, Tuple0[] memory tokens)
-		external
-		returns (bool);
-}
-
 // 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,
@@ -70,6 +59,12 @@
 		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,
@@ -77,10 +72,19 @@
 		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;
 
-	// Throws error if key not found
+	// @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)
@@ -91,7 +95,10 @@
 
 // Selector: 42966c68
 interface ERC721Burnable is Dummy, ERC165 {
-	// @dev Not implemented
+	// @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;
@@ -99,6 +106,12 @@
 
 // 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);
 
@@ -124,7 +137,17 @@
 		uint256 tokenId
 	) external;
 
-	// @dev Not implemented
+	// @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(
@@ -159,13 +182,25 @@
 
 // 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);
 
-	// Returns token's const_metadata
+	// @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);
@@ -176,14 +211,21 @@
 	// Selector: mintingFinished() 05d2035b
 	function mintingFinished() external view returns (bool);
 
-	// `token_id` should be obtained with `next_token_id` method,
-	// unlike standard, you can't specify it manually
+	// @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);
 
-	// `token_id` should be obtained with `next_token_id` method,
-	// unlike standard, you can't specify it manually
+	// @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(
@@ -200,6 +242,11 @@
 
 // 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);
 
@@ -211,6 +258,10 @@
 		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);
 }
@@ -363,6 +414,59 @@
 	function setCollectionMintMode(bool mode) external;
 }
 
+// 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,
modifiedtests/src/eth/reFungible.test.tsdiffbeforeafterboth
--- a/tests/src/eth/reFungible.test.ts
+++ b/tests/src/eth/reFungible.test.ts
@@ -14,8 +14,8 @@
 // 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} from '../util/helpers';
-import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, evmCollection, evmCollectionHelpers, GAS_ARGS, getCollectionAddressFromResult, itWeb3, normalizeEvents, tokenIdToAddress} from './util/helpers';
+import {createCollectionExpectSuccess, UNIQUE} from '../util/helpers';
+import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, evmCollection, evmCollectionHelpers, GAS_ARGS, getCollectionAddressFromResult, itWeb3, normalizeEvents, recordEthFee, tokenIdToAddress} from './util/helpers';
 import reFungibleAbi from './reFungibleAbi.json';
 import reFungibleTokenAbi from './reFungibleTokenAbi.json';
 import {expect} from 'chai';
@@ -26,7 +26,7 @@
     const helper = evmCollectionHelpers(web3, caller);
     const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
     const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
-    const contract = new web3.eth.Contract(reFungibleAbi as any, collectionIdAddress, {from: caller, ...GAS_ARGS});
+    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();
@@ -38,7 +38,7 @@
     const helper = evmCollectionHelpers(web3, caller);
     const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
     const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
-    const contract = new web3.eth.Contract(reFungibleAbi as any, collectionIdAddress, {from: caller, ...GAS_ARGS});
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
 
     {
       const nextTokenId = await contract.methods.nextTokenId().call();
@@ -63,7 +63,7 @@
     const helper = evmCollectionHelpers(web3, caller);
     const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
     const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
-    const contract = new web3.eth.Contract(reFungibleAbi as any, collectionIdAddress, {from: caller, ...GAS_ARGS});
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
 
     const tokenId = await contract.methods.nextTokenId().call();
     await contract.methods.mint(caller, tokenId).send();
@@ -79,7 +79,7 @@
     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 = new web3.eth.Contract(reFungibleAbi as any, collectionIdAddress, {from: caller, ...GAS_ARGS});
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
 
     const tokenId = await contract.methods.nextTokenId().call();
     await contract.methods.mint(caller, tokenId).send();
@@ -105,7 +105,7 @@
     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);
+    const contract = evmCollection(web3, owner, collectionIdAddress, {type: 'ReFungible'});
     const nextTokenId = await contract.methods.nextTokenId().call();
 
     expect(nextTokenId).to.be.equal('1');
@@ -137,7 +137,7 @@
     const helper = evmCollectionHelpers(web3, caller);
     const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
     const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
-    const contract = new web3.eth.Contract(reFungibleAbi as any, collectionIdAddress, {from: caller, ...GAS_ARGS});
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
 
     const receiver = createEthAccount(web3);
 
@@ -189,8 +189,148 @@
       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.be.deep.equal([
+        {
+          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} = 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.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(),
+          },
+        },
+      ]);
+    }
+
+    {
+      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);
+    }
+  });
 });
 
+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({
@@ -200,7 +340,7 @@
     const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
 
     const address = collectionIdToAddress(collection);
-    const contract = new web3.eth.Contract(reFungibleAbi as any, address, {from: caller, ...GAS_ARGS});
+    const contract = evmCollection(web3, caller, address, {type: 'ReFungible'});
     const name = await contract.methods.name().call();
 
     expect(name).to.equal('token name');
@@ -214,7 +354,7 @@
     const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
 
     const address = collectionIdToAddress(collection);
-    const contract = new web3.eth.Contract(reFungibleAbi as any, address, {from: caller, ...GAS_ARGS});
+    const contract = evmCollection(web3, caller, address, {type: 'ReFungible'});
     const symbol = await contract.methods.symbol().call();
 
     expect(symbol).to.equal('TOK');
modifiedtests/src/eth/reFungibleAbi.jsondiffbeforeafterboth
--- a/tests/src/eth/reFungibleAbi.json
+++ b/tests/src/eth/reFungibleAbi.json
@@ -136,6 +136,16 @@
     "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" }],
@@ -498,6 +508,16 @@
   },
   {
     "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" }