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
--- a/pallets/refungible/CHANGELOG.md
+++ b/pallets/refungible/CHANGELOG.md
@@ -2,8 +2,18 @@
 
 All notable changes to this project will be documented in this file.
 
+## [v0.2.1] - 2022-07-27
+
+### New features
+
+Implementation of ERC-721 EVM API ([#452](https://github.com/UniqueNetwork/unique-chain/pull/452))
+
 ## [v0.2.0] - 2022-08-01
+
 ### Deprecated
+
+`const_data` field is removed
+
 - `ItemData`
 - `TokenData`
 
@@ -15,11 +25,13 @@
 test(refungible-pallet): add tests for ERC-20 EVM API for RFT token pieces ([#413](https://github.com/UniqueNetwork/unique-chain/pull/413))
 
 ## [v0.1.1] - 2022-07-14
-### Added
+
+### Added features
+
 - Support for properties for RFT collections and tokens.
 
 ### Other changes
 
 - feat: RPC method `token_owners` returning 10 owners in no particular order.
 
-This was an internal request to improve the web interface and support fractionalization event. 
+This was an internal request to improve the web interface and support fractionalization event.
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
14// You should have received a copy of the GNU General Public License14// You should have received a copy of the GNU General Public License
15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
16
17//! # Refungible Pallet EVM API for tokens
18//!
19//! Provides ERC-721 standart support implementation and EVM API for unique extensions for Refungible Pallet.
20//! Method implementations are mostly doing parameter conversion and calling Refungible Pallet methods.
1621
17extern crate alloc;22extern crate alloc;
23
24use alloc::string::ToString;
25use core::{
26 char::{REPLACEMENT_CHARACTER, decode_utf16},
27 convert::TryInto,
28};
18use evm_coder::{generate_stubgen, solidity_interface, types::*};29use evm_coder::{ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*, weight};
1930use frame_support::{BoundedBTreeMap, BoundedVec};
20use pallet_common::{CollectionHandle, erc::CollectionCall, erc::CommonEvmHandler};31use pallet_common::{
2132 CollectionHandle, CollectionPropertyPermissions,
33 erc::{
34 CommonEvmHandler, CollectionCall,
35 static_property::{key, value as property_value},
36 },
37};
22use pallet_evm::PrecompileHandle;38use pallet_evm::{account::CrossAccountId, PrecompileHandle};
23use pallet_evm_coder_substrate::call;39use pallet_evm_coder_substrate::{call, dispatch_to_evm};
40use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};
41use sp_core::H160;
42use sp_std::{collections::btree_map::BTreeMap, vec::Vec, vec};
43use up_data_structs::{
44 CollectionId, CollectionPropertiesVec, Property, PropertyKey, PropertyKeyPermission,
45 PropertyPermission, TokenId,
46};
2447
25use crate::{Config, RefungibleHandle};48use crate::{
49 AccountBalance, Balance, Config, CreateItemData, Pallet, RefungibleHandle, SelfWeightOf,
50 TokenProperties, TokensMinted, TotalSupply, weights::WeightInfo,
51};
52
53pub const ADDRESS_FOR_PARTIALLY_OWNED_TOKENS: H160 = H160::repeat_byte(0xff);
54
55/// @title A contract that allows to set and delete token properties and change token property permissions.
56#[solidity_interface(name = "TokenProperties")]
57impl<T: Config> RefungibleHandle<T> {
58 /// @notice Set permissions for token property.
59 /// @dev Throws error if `msg.sender` is not admin or owner of the collection.
60 /// @param key Property key.
61 /// @param is_mutable Permission to mutate property.
62 /// @param collection_admin Permission to mutate property by collection admin if property is mutable.
63 /// @param token_owner Permission to mutate property by token owner if property is mutable.
64 fn set_token_property_permission(
65 &mut self,
66 caller: caller,
67 key: string,
68 is_mutable: bool,
69 collection_admin: bool,
70 token_owner: bool,
71 ) -> Result<()> {
72 let caller = T::CrossAccountId::from_eth(caller);
73 <Pallet<T>>::set_token_property_permissions(
74 self,
75 &caller,
76 vec![PropertyKeyPermission {
77 key: <Vec<u8>>::from(key)
78 .try_into()
79 .map_err(|_| "too long key")?,
80 permission: PropertyPermission {
81 mutable: is_mutable,
82 collection_admin,
83 token_owner,
84 },
85 }],
86 )
87 .map_err(dispatch_to_evm::<T>)
88 }
89
90 /// @notice Set token property value.
91 /// @dev Throws error if `msg.sender` has no permission to edit the property.
92 /// @param tokenId ID of the token.
93 /// @param key Property key.
94 /// @param value Property value.
95 fn set_property(
96 &mut self,
97 caller: caller,
98 token_id: uint256,
99 key: string,
100 value: bytes,
101 ) -> Result<()> {
102 let caller = T::CrossAccountId::from_eth(caller);
103 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
104 let key = <Vec<u8>>::from(key)
105 .try_into()
106 .map_err(|_| "key too long")?;
107 let value = value.try_into().map_err(|_| "value too long")?;
108
109 let nesting_budget = self
110 .recorder
111 .weight_calls_budget(<StructureWeight<T>>::find_parent());
112
113 <Pallet<T>>::set_token_property(
114 self,
115 &caller,
116 TokenId(token_id),
117 Property { key, value },
118 &nesting_budget,
119 )
120 .map_err(dispatch_to_evm::<T>)
121 }
122
123 /// @notice Delete token property value.
124 /// @dev Throws error if `msg.sender` has no permission to edit the property.
125 /// @param tokenId ID of the token.
126 /// @param key Property key.
127 fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {
128 let caller = T::CrossAccountId::from_eth(caller);
129 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
130 let key = <Vec<u8>>::from(key)
131 .try_into()
132 .map_err(|_| "key too long")?;
133
134 let nesting_budget = self
135 .recorder
136 .weight_calls_budget(<StructureWeight<T>>::find_parent());
137
138 <Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)
139 .map_err(dispatch_to_evm::<T>)
140 }
141
142 /// @notice Get token property value.
143 /// @dev Throws error if key not found
144 /// @param tokenId ID of the token.
145 /// @param key Property key.
146 /// @return Property value bytes
147 fn property(&self, token_id: uint256, key: string) -> Result<bytes> {
148 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
149 let key = <Vec<u8>>::from(key)
150 .try_into()
151 .map_err(|_| "key too long")?;
152
153 let props = <TokenProperties<T>>::get((self.id, token_id));
154 let prop = props.get(&key).ok_or("key not found")?;
155
156 Ok(prop.to_vec())
157 }
158}
159
160#[derive(ToLog)]
161pub enum ERC721Events {
162 /// @dev This event emits when NFTs are created (`from` == 0) and destroyed
163 /// (`to` == 0). Exception: during contract creation, any number of RFTs
164 /// may be created and assigned without emitting Transfer.
165 Transfer {
166 #[indexed]
167 from: address,
168 #[indexed]
169 to: address,
170 #[indexed]
171 token_id: uint256,
172 },
173 /// @dev Not supported
174 Approval {
175 #[indexed]
176 owner: address,
177 #[indexed]
178 approved: address,
179 #[indexed]
180 token_id: uint256,
181 },
182 /// @dev Not supported
183 #[allow(dead_code)]
184 ApprovalForAll {
185 #[indexed]
186 owner: address,
187 #[indexed]
188 operator: address,
189 approved: bool,
190 },
191}
192
193#[derive(ToLog)]
194pub enum ERC721MintableEvents {
195 /// @dev Not supported
196 #[allow(dead_code)]
197 MintingFinished {},
198}
199
200#[solidity_interface(name = "ERC721Metadata")]
201impl<T: Config> RefungibleHandle<T> {
202 /// @notice A descriptive name for a collection of RFTs in this contract
203 fn name(&self) -> Result<string> {
204 Ok(decode_utf16(self.name.iter().copied())
205 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))
206 .collect::<string>())
207 }
208
209 /// @notice An abbreviated name for RFTs in this contract
210 fn symbol(&self) -> Result<string> {
211 Ok(string::from_utf8_lossy(&self.token_prefix).into())
212 }
213
214 /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
215 ///
216 /// @dev If the token has a `url` property and it is not empty, it is returned.
217 /// 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`.
218 /// If the collection property `baseURI` is empty or absent, return "" (empty string)
219 /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix
220 /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).
221 ///
222 /// @return token's const_metadata
223 #[solidity(rename_selector = "tokenURI")]
224 fn token_uri(&self, token_id: uint256) -> Result<string> {
225 let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
226
227 if let Ok(url) = get_token_property(self, token_id_u32, &key::url()) {
228 if !url.is_empty() {
229 return Ok(url);
230 }
231 } else if !is_erc721_metadata_compatible::<T>(self.id) {
232 return Err("tokenURI not set".into());
233 }
234
235 if let Some(base_uri) =
236 pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())
237 {
238 if !base_uri.is_empty() {
239 let base_uri = string::from_utf8(base_uri.into_inner()).map_err(|e| {
240 Error::Revert(alloc::format!(
241 "Can not convert value \"baseURI\" to string with error \"{}\"",
242 e
243 ))
244 })?;
245 if let Ok(suffix) = get_token_property(self, token_id_u32, &key::suffix()) {
246 if !suffix.is_empty() {
247 return Ok(base_uri + suffix.as_str());
248 }
249 }
250
251 return Ok(base_uri + token_id.to_string().as_str());
252 }
253 }
254
255 Ok("".into())
256 }
257}
258
259/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
260/// @dev See https://eips.ethereum.org/EIPS/eip-721
261#[solidity_interface(name = "ERC721Enumerable")]
262impl<T: Config> RefungibleHandle<T> {
263 /// @notice Enumerate valid RFTs
264 /// @param index A counter less than `totalSupply()`
265 /// @return The token identifier for the `index`th NFT,
266 /// (sort order not specified)
267 fn token_by_index(&self, index: uint256) -> Result<uint256> {
268 Ok(index)
269 }
270
271 /// Not implemented
272 fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {
273 // TODO: Not implemetable
274 Err("not implemented".into())
275 }
276
277 /// @notice Count RFTs tracked by this contract
278 /// @return A count of valid RFTs tracked by this contract, where each one of
279 /// them has an assigned and queryable owner not equal to the zero address
280 fn total_supply(&self) -> Result<uint256> {
281 self.consume_store_reads(1)?;
282 Ok(<Pallet<T>>::total_supply(self).into())
283 }
284}
285
286/// @title ERC-721 Non-Fungible Token Standard
287/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
288#[solidity_interface(name = "ERC721", events(ERC721Events))]
289impl<T: Config> RefungibleHandle<T> {
290 /// @notice Count all RFTs assigned to an owner
291 /// @dev RFTs assigned to the zero address are considered invalid, and this
292 /// function throws for queries about the zero address.
293 /// @param owner An address for whom to query the balance
294 /// @return The number of RFTs owned by `owner`, possibly zero
295 fn balance_of(&self, owner: address) -> Result<uint256> {
296 self.consume_store_reads(1)?;
297 let owner = T::CrossAccountId::from_eth(owner);
298 let balance = <AccountBalance<T>>::get((self.id, owner));
299 Ok(balance.into())
300 }
301
302 /// @notice Find the owner of an RFT
303 /// @dev RFTs assigned to zero address are considered invalid, and queries
304 /// about them do throw.
305 /// Returns special 0xffffffffffffffffffffffffffffffffffffffff address for
306 /// the tokens that are partially owned.
307 /// @param tokenId The identifier for an RFT
308 /// @return The address of the owner of the RFT
309 fn owner_of(&self, token_id: uint256) -> Result<address> {
310 self.consume_store_reads(2)?;
311 let token = token_id.try_into()?;
312 let owner = <Pallet<T>>::token_owner(self.id, token);
313 Ok(owner
314 .map(|address| *address.as_eth())
315 .unwrap_or_else(|| ADDRESS_FOR_PARTIALLY_OWNED_TOKENS))
316 }
317
318 /// @dev Not implemented
319 fn safe_transfer_from_with_data(
320 &mut self,
321 _from: address,
322 _to: address,
323 _token_id: uint256,
324 _data: bytes,
325 _value: value,
326 ) -> Result<void> {
327 // TODO: Not implemetable
328 Err("not implemented".into())
329 }
330
331 /// @dev Not implemented
332 fn safe_transfer_from(
333 &mut self,
334 _from: address,
335 _to: address,
336 _token_id: uint256,
337 _value: value,
338 ) -> Result<void> {
339 // TODO: Not implemetable
340 Err("not implemented".into())
341 }
342
343 /// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE
344 /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE
345 /// THEY MAY BE PERMANENTLY LOST
346 /// @dev Throws unless `msg.sender` is the current owner or an authorized
347 /// operator for this RFT. Throws if `from` is not the current owner. Throws
348 /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT.
349 /// Throws if RFT pieces have multiple owners.
350 /// @param from The current owner of the NFT
351 /// @param to The new owner
352 /// @param tokenId The NFT to transfer
353 /// @param _value Not used for an NFT
354 #[weight(<SelfWeightOf<T>>::transfer_from_creating_removing())]
355 fn transfer_from(
356 &mut self,
357 caller: caller,
358 from: address,
359 to: address,
360 token_id: uint256,
361 _value: value,
362 ) -> Result<void> {
363 let caller = T::CrossAccountId::from_eth(caller);
364 let from = T::CrossAccountId::from_eth(from);
365 let to = T::CrossAccountId::from_eth(to);
366 let token = token_id.try_into()?;
367 let budget = self
368 .recorder
369 .weight_calls_budget(<StructureWeight<T>>::find_parent());
370
371 let balance = balance(&self, token, &from)?;
372 ensure_single_owner(&self, token, balance)?;
373
374 <Pallet<T>>::transfer_from(self, &caller, &from, &to, token, balance, &budget)
375 .map_err(dispatch_to_evm::<T>)?;
376
377 Ok(())
378 }
379
380 /// @dev Not implemented
381 fn approve(
382 &mut self,
383 _caller: caller,
384 _approved: address,
385 _token_id: uint256,
386 _value: value,
387 ) -> Result<void> {
388 Err("not implemented".into())
389 }
390
391 /// @dev Not implemented
392 fn set_approval_for_all(
393 &mut self,
394 _caller: caller,
395 _operator: address,
396 _approved: bool,
397 ) -> Result<void> {
398 // TODO: Not implemetable
399 Err("not implemented".into())
400 }
401
402 /// @dev Not implemented
403 fn get_approved(&self, _token_id: uint256) -> Result<address> {
404 // TODO: Not implemetable
405 Err("not implemented".into())
406 }
407
408 /// @dev Not implemented
409 fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {
410 // TODO: Not implemetable
411 Err("not implemented".into())
412 }
413}
414
415/// Returns amount of pieces of `token` that `owner` have
416fn balance<T: Config>(
417 collection: &RefungibleHandle<T>,
418 token: TokenId,
419 owner: &T::CrossAccountId,
420) -> Result<u128> {
421 collection.consume_store_reads(1)?;
422 let balance = <Balance<T>>::get((collection.id, token, &owner));
423 Ok(balance)
424}
425
426/// Throws if `owner_balance` is lower than total amount of `token` pieces
427fn ensure_single_owner<T: Config>(
428 collection: &RefungibleHandle<T>,
429 token: TokenId,
430 owner_balance: u128,
431) -> Result<()> {
432 collection.consume_store_reads(1)?;
433 let total_supply = <TotalSupply<T>>::get((collection.id, token));
434 if total_supply != owner_balance {
435 return Err("token has multiple owners".into());
436 }
437 Ok(())
438}
439
440/// @title ERC721 Token that can be irreversibly burned (destroyed).
441#[solidity_interface(name = "ERC721Burnable")]
442impl<T: Config> RefungibleHandle<T> {
443 /// @notice Burns a specific ERC721 token.
444 /// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized
445 /// operator of the current owner.
446 /// @param tokenId The RFT to approve
447 #[weight(<SelfWeightOf<T>>::burn_item_fully())]
448 fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {
449 let caller = T::CrossAccountId::from_eth(caller);
450 let token = token_id.try_into()?;
451
452 let balance = balance(&self, token, &caller)?;
453 ensure_single_owner(&self, token, balance)?;
454
455 <Pallet<T>>::burn(self, &caller, token, balance).map_err(dispatch_to_evm::<T>)?;
456 Ok(())
457 }
458}
459
460/// @title ERC721 minting logic.
461#[solidity_interface(name = "ERC721Mintable", events(ERC721MintableEvents))]
462impl<T: Config> RefungibleHandle<T> {
463 fn minting_finished(&self) -> Result<bool> {
464 Ok(false)
465 }
466
467 /// @notice Function to mint token.
468 /// @dev `tokenId` should be obtained with `nextTokenId` method,
469 /// unlike standard, you can't specify it manually
470 /// @param to The new owner
471 /// @param tokenId ID of the minted RFT
472 #[weight(<SelfWeightOf<T>>::create_item())]
473 fn mint(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {
474 let caller = T::CrossAccountId::from_eth(caller);
475 let to = T::CrossAccountId::from_eth(to);
476 let token_id: u32 = token_id.try_into()?;
477 let budget = self
478 .recorder
479 .weight_calls_budget(<StructureWeight<T>>::find_parent());
480
481 if <TokensMinted<T>>::get(self.id)
482 .checked_add(1)
483 .ok_or("item id overflow")?
484 != token_id
485 {
486 return Err("item id should be next".into());
487 }
488
489 let users = [(to.clone(), 1)]
490 .into_iter()
491 .collect::<BTreeMap<_, _>>()
492 .try_into()
493 .unwrap();
494 <Pallet<T>>::create_item(
495 self,
496 &caller,
497 CreateItemData::<T> {
498 users,
499 properties: CollectionPropertiesVec::default(),
500 },
501 &budget,
502 )
503 .map_err(dispatch_to_evm::<T>)?;
504
505 Ok(true)
506 }
507
508 /// @notice Function to mint token with the given tokenUri.
509 /// @dev `tokenId` should be obtained with `nextTokenId` method,
510 /// unlike standard, you can't specify it manually
511 /// @param to The new owner
512 /// @param tokenId ID of the minted RFT
513 /// @param tokenUri Token URI that would be stored in the RFT properties
514 #[solidity(rename_selector = "mintWithTokenURI")]
515 #[weight(<SelfWeightOf<T>>::create_item())]
516 fn mint_with_token_uri(
517 &mut self,
518 caller: caller,
519 to: address,
520 token_id: uint256,
521 token_uri: string,
522 ) -> Result<bool> {
523 let key = key::url();
524 let permission = get_token_permission::<T>(self.id, &key)?;
525 if !permission.collection_admin {
526 return Err("Operation is not allowed".into());
527 }
528
529 let caller = T::CrossAccountId::from_eth(caller);
530 let to = T::CrossAccountId::from_eth(to);
531 let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;
532 let budget = self
533 .recorder
534 .weight_calls_budget(<StructureWeight<T>>::find_parent());
535
536 if <TokensMinted<T>>::get(self.id)
537 .checked_add(1)
538 .ok_or("item id overflow")?
539 != token_id
540 {
541 return Err("item id should be next".into());
542 }
543
544 let mut properties = CollectionPropertiesVec::default();
545 properties
546 .try_push(Property {
547 key,
548 value: token_uri
549 .into_bytes()
550 .try_into()
551 .map_err(|_| "token uri is too long")?,
552 })
553 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;
554
555 let users = [(to.clone(), 1)]
556 .into_iter()
557 .collect::<BTreeMap<_, _>>()
558 .try_into()
559 .unwrap();
560 <Pallet<T>>::create_item(
561 self,
562 &caller,
563 CreateItemData::<T> {
564 users,
565 properties,
566 },
567 &budget,
568 )
569 .map_err(dispatch_to_evm::<T>)?;
570 Ok(true)
571 }
572
573 /// @dev Not implemented
574 fn finish_minting(&mut self, _caller: caller) -> Result<bool> {
575 Err("not implementable".into())
576 }
577}
578
579fn get_token_property<T: Config>(
580 collection: &CollectionHandle<T>,
581 token_id: u32,
582 key: &up_data_structs::PropertyKey,
583) -> Result<string> {
584 collection.consume_store_reads(1)?;
585 let properties = <TokenProperties<T>>::try_get((collection.id, token_id))
586 .map_err(|_| Error::Revert("Token properties not found".into()))?;
587 if let Some(property) = properties.get(key) {
588 return Ok(string::from_utf8_lossy(property).into());
589 }
590
591 Err("Property tokenURI not found".into())
592}
593
594fn is_erc721_metadata_compatible<T: Config>(collection_id: CollectionId) -> bool {
595 if let Some(shema_name) =
596 pallet_common::Pallet::<T>::get_collection_property(collection_id, &key::schema_name())
597 {
598 let shema_name = shema_name.into_inner();
599 shema_name == property_value::ERC721_METADATA
600 } else {
601 false
602 }
603}
604
605fn get_token_permission<T: Config>(
606 collection_id: CollectionId,
607 key: &PropertyKey,
608) -> Result<PropertyPermission> {
609 let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)
610 .map_err(|_| Error::Revert("No permissions for collection".into()))?;
611 let a = token_property_permissions
612 .get(key)
613 .map(Clone::clone)
614 .ok_or_else(|| {
615 let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();
616 Error::Revert(alloc::format!("No permission for key {}", key))
617 })?;
618 Ok(a)
619}
620
621/// @title Unique extensions for ERC721.
622#[solidity_interface(name = "ERC721UniqueExtensions")]
623impl<T: Config> RefungibleHandle<T> {
624 /// @notice Transfer ownership of an RFT
625 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`
626 /// is the zero address. Throws if `tokenId` is not a valid RFT.
627 /// Throws if RFT pieces have multiple owners.
628 /// @param to The new owner
629 /// @param tokenId The RFT to transfer
630 /// @param _value Not used for an RFT
631 #[weight(<SelfWeightOf<T>>::transfer_creating_removing())]
632 fn transfer(
633 &mut self,
634 caller: caller,
635 to: address,
636 token_id: uint256,
637 _value: value,
638 ) -> Result<void> {
639 let caller = T::CrossAccountId::from_eth(caller);
640 let to = T::CrossAccountId::from_eth(to);
641 let token = token_id.try_into()?;
642 let budget = self
643 .recorder
644 .weight_calls_budget(<StructureWeight<T>>::find_parent());
645
646 let balance = balance(&self, token, &caller)?;
647 ensure_single_owner(&self, token, balance)?;
648
649 <Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)
650 .map_err(dispatch_to_evm::<T>)?;
651 Ok(())
652 }
653
654 /// @notice Burns a specific ERC721 token.
655 /// @dev Throws unless `msg.sender` is the current owner or an authorized
656 /// operator for this RFT. Throws if `from` is not the current owner. Throws
657 /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT.
658 /// Throws if RFT pieces have multiple owners.
659 /// @param from The current owner of the RFT
660 /// @param tokenId The RFT to transfer
661 /// @param _value Not used for an RFT
662 #[weight(<SelfWeightOf<T>>::burn_from())]
663 fn burn_from(
664 &mut self,
665 caller: caller,
666 from: address,
667 token_id: uint256,
668 _value: value,
669 ) -> Result<void> {
670 let caller = T::CrossAccountId::from_eth(caller);
671 let from = T::CrossAccountId::from_eth(from);
672 let token = token_id.try_into()?;
673 let budget = self
674 .recorder
675 .weight_calls_budget(<StructureWeight<T>>::find_parent());
676
677 let balance = balance(&self, token, &caller)?;
678 ensure_single_owner(&self, token, balance)?;
679
680 <Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)
681 .map_err(dispatch_to_evm::<T>)?;
682 Ok(())
683 }
684
685 /// @notice Returns next free RFT ID.
686 fn next_token_id(&self) -> Result<uint256> {
687 self.consume_store_reads(1)?;
688 Ok(<TokensMinted<T>>::get(self.id)
689 .checked_add(1)
690 .ok_or("item id overflow")?
691 .into())
692 }
693
694 /// @notice Function to mint multiple tokens.
695 /// @dev `tokenIds` should be an array of consecutive numbers and first number
696 /// should be obtained with `nextTokenId` method
697 /// @param to The new owner
698 /// @param tokenIds IDs of the minted RFTs
699 #[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]
700 fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {
701 let caller = T::CrossAccountId::from_eth(caller);
702 let to = T::CrossAccountId::from_eth(to);
703 let mut expected_index = <TokensMinted<T>>::get(self.id)
704 .checked_add(1)
705 .ok_or("item id overflow")?;
706 let budget = self
707 .recorder
708 .weight_calls_budget(<StructureWeight<T>>::find_parent());
709
710 let total_tokens = token_ids.len();
711 for id in token_ids.into_iter() {
712 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;
713 if id != expected_index {
714 return Err("item id should be next".into());
715 }
716 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;
717 }
718 let users = [(to.clone(), 1)]
719 .into_iter()
720 .collect::<BTreeMap<_, _>>()
721 .try_into()
722 .unwrap();
723 let create_item_data = CreateItemData::<T> {
724 users,
725 properties: CollectionPropertiesVec::default(),
726 };
727 let data = (0..total_tokens)
728 .map(|_| create_item_data.clone())
729 .collect();
730
731 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)
732 .map_err(dispatch_to_evm::<T>)?;
733 Ok(true)
734 }
735
736 /// @notice Function to mint multiple tokens with the given tokenUris.
737 /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive
738 /// numbers and first number should be obtained with `nextTokenId` method
739 /// @param to The new owner
740 /// @param tokens array of pairs of token ID and token URI for minted tokens
741 #[solidity(rename_selector = "mintBulkWithTokenURI")]
742 #[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]
743 fn mint_bulk_with_token_uri(
744 &mut self,
745 caller: caller,
746 to: address,
747 tokens: Vec<(uint256, string)>,
748 ) -> Result<bool> {
749 let key = key::url();
750 let caller = T::CrossAccountId::from_eth(caller);
751 let to = T::CrossAccountId::from_eth(to);
752 let mut expected_index = <TokensMinted<T>>::get(self.id)
753 .checked_add(1)
754 .ok_or("item id overflow")?;
755 let budget = self
756 .recorder
757 .weight_calls_budget(<StructureWeight<T>>::find_parent());
758
759 let mut data = Vec::with_capacity(tokens.len());
760 let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]
761 .into_iter()
762 .collect::<BTreeMap<_, _>>()
763 .try_into()
764 .unwrap();
765 for (id, token_uri) in tokens {
766 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;
767 if id != expected_index {
768 return Err("item id should be next".into());
769 }
770 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;
771
772 let mut properties = CollectionPropertiesVec::default();
773 properties
774 .try_push(Property {
775 key: key.clone(),
776 value: token_uri
777 .into_bytes()
778 .try_into()
779 .map_err(|_| "token uri is too long")?,
780 })
781 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;
782
783 let create_item_data = CreateItemData::<T> {
784 users: users.clone(),
785 properties,
786 };
787 data.push(create_item_data);
788 }
789
790 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)
791 .map_err(dispatch_to_evm::<T>)?;
792 Ok(true)
793 }
794}
26795
27#[solidity_interface(796#[solidity_interface(
28 name = "UniqueRefungible",797 name = "UniqueRefungible",
29 is(via("CollectionHandle<T>", common_mut, Collection),)798 is(
799 ERC721,
800 ERC721Metadata,
801 ERC721Enumerable,
802 ERC721UniqueExtensions,
803 ERC721Mintable,
804 ERC721Burnable,
805 via("CollectionHandle<T>", common_mut, Collection),
806 TokenProperties,
807 )
30)]808)]
31impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> {}809impl<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};