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

difftreelog

misk: Move TokenChanged event into ERC721 contracts

Trubnikov Sergey2023-02-02parent: #3563d09.patch.diff
in: master

16 files changed

modifiedpallets/common/src/erc.rsdiffbeforeafterboth
--- a/pallets/common/src/erc.rs
+++ b/pallets/common/src/erc.rs
@@ -26,7 +26,6 @@
 };
 use pallet_evm_coder_substrate::dispatch_to_evm;
 use sp_std::{vec, vec::Vec};
-use sp_core::U256;
 use up_data_structs::{
 	CollectionMode, CollectionPermissions, OwnerRestrictedSet, Property, SponsoringRateLimit,
 	SponsorshipState,
@@ -62,18 +61,9 @@
 	},
 	/// The collection has been changed.
 	CollectionChanged {
-		/// Collection ID.
-		#[indexed]
-		collection_id: Address,
-	},
-
-	/// The token has been changed.
-	TokenChanged {
 		/// Collection ID.
 		#[indexed]
 		collection_id: Address,
-		/// Token ID.
-		token_id: U256,
 	},
 }
 
modifiedpallets/common/src/lib.rsdiffbeforeafterboth
--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -1250,6 +1250,7 @@
 		mut stored_properties: Properties,
 		is_token_owner: impl Fn() -> Result<bool, DispatchError>,
 		set_token_properties: impl FnOnce(Properties),
+		log: evm_coder::ethereum::Log,
 	) -> DispatchResult {
 		let is_collection_admin = collection.is_owner_or_admin(sender);
 		let permissions = Self::property_permissions(collection.id);
@@ -1304,13 +1305,7 @@
 				}
 			}
 
-			<PalletEvm<T>>::deposit_log(
-				CollectionHelpersEvents::TokenChanged {
-					collection_id: eth::collection_id_to_address(collection.id),
-					token_id: token_id.into(),
-				}
-				.to_log(T::ContractAddress::get()),
-			);
+			<PalletEvm<T>>::deposit_log(log.clone());
 		}
 
 		set_token_properties(stored_properties);
modifiedpallets/nonfungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/erc.rs
+++ b/pallets/nonfungible/src/erc.rs
@@ -50,13 +50,26 @@
 	TokenProperties, SelfWeightOf, weights::WeightInfo,
 };
 
+/// Nft events.
+#[derive(ToLog)]
+pub enum ERC721TokenEvent {
+	/// The token has been changed.
+	TokenChanged {
+		/// Collection ID.
+		#[indexed]
+		collection_id: Address,
+		/// Token ID.
+		token_id: U256,
+	},
+}
+
 frontier_contract! {
 	macro_rules! NonfungibleHandle_result {...}
 	impl<T: Config> Contract for NonfungibleHandle<T> {...}
 }
 
 /// @title A contract that allows to set and delete token properties and change token property permissions.
-#[solidity_interface(name = TokenProperties, enum(derive(PreDispatch)), enum_attr(weight))]
+#[solidity_interface(name = TokenProperties, events(ERC721TokenEvent), enum(derive(PreDispatch)), enum_attr(weight))]
 impl<T: Config> NonfungibleHandle<T> {
 	/// @notice Set permissions for token property.
 	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
modifiedpallets/nonfungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/lib.rs
+++ b/pallets/nonfungible/src/lib.rs
@@ -112,7 +112,7 @@
 };
 use pallet_structure::{Pallet as PalletStructure, Error as StructureError};
 use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};
-use sp_core::H160;
+use sp_core::{Get, H160};
 use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};
 use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};
 use core::ops::Deref;
@@ -622,6 +622,11 @@
 			stored_properties,
 			is_token_owner,
 			|properties| <TokenProperties<T>>::set((collection.id, token_id), properties),
+			erc::ERC721TokenEvent::TokenChanged {
+				collection_id: collection_id_to_address(collection.id),
+				token_id: token_id.into(),
+			}
+			.to_log(T::ContractAddress::get()),
 		)
 	}
 
modifiedpallets/nonfungible/src/stubs/UniqueNFT.soldiffbeforeafterboth
--- a/pallets/nonfungible/src/stubs/UniqueNFT.sol
+++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol
@@ -17,9 +17,14 @@
 	}
 }
 
+/// @dev inlined interface
+contract ERC721TokenEvent {
+	event TokenChanged(address indexed collectionId, uint256 tokenId);
+}
+
 /// @title A contract that allows to set and delete token properties and change token property permissions.
 /// @dev the ERC-165 identifier for this interface is 0xde0695c2
-contract TokenProperties is Dummy, ERC165 {
+contract TokenProperties is Dummy, ERC165, ERC721TokenEvent {
 	// /// @notice Set permissions for token property.
 	// /// @dev Throws error if `msg.sender` is not admin or owner of the collection.
 	// /// @param key Property key.
modifiedpallets/refungible/src/erc.rsdiffbeforeafterboth
before · pallets/refungible/src/erc.rs
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Refungible Pallet EVM API for tokens18//!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.2122extern crate alloc;2324use core::{25	char::{REPLACEMENT_CHARACTER, decode_utf16},26	convert::TryInto,27};28use evm_coder::{abi::AbiType, ToLog, generate_stubgen, solidity_interface, types::*};29use frame_support::{BoundedBTreeMap, BoundedVec};30use pallet_common::{31	CollectionHandle, CollectionPropertyPermissions, CommonCollectionOperations,32	Error as CommonError,33	erc::{CommonEvmHandler, CollectionCall, static_property::key},34	eth::{self, TokenUri},35};36use pallet_evm::{account::CrossAccountId, PrecompileHandle};37use pallet_evm_coder_substrate::{38	call, dispatch_to_evm,39	execution::{PreDispatch, Result, Error},40	frontier_contract,41};42use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};43use sp_core::{H160, U256, Get};44use sp_std::{collections::btree_map::BTreeMap, vec::Vec, vec};45use up_data_structs::{46	CollectionId, CollectionPropertiesVec, mapping::TokenAddressMapping, Property, PropertyKey,47	PropertyKeyPermission, PropertyPermission, TokenId, TokenOwnerError,48};4950use crate::{51	AccountBalance, Balance, Config, CreateItemData, Pallet, RefungibleHandle, TokenProperties,52	TokensMinted, TotalSupply, SelfWeightOf, weights::WeightInfo,53};5455frontier_contract! {56	macro_rules! RefungibleHandle_result {...}57	impl<T: Config> Contract for RefungibleHandle<T> {...}58}5960pub const ADDRESS_FOR_PARTIALLY_OWNED_TOKENS: H160 = H160::repeat_byte(0xff);6162/// @title A contract that allows to set and delete token properties and change token property permissions.63#[solidity_interface(name = TokenProperties, enum(derive(PreDispatch)), enum_attr(weight))]64impl<T: Config> RefungibleHandle<T> {65	/// @notice Set permissions for token property.66	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.67	/// @param key Property key.68	/// @param isMutable Permission to mutate property.69	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.70	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.71	#[solidity(hide)]72	#[weight(<SelfWeightOf<T>>::set_token_property_permissions(1))]73	fn set_token_property_permission(74		&mut self,75		caller: Caller,76		key: String,77		is_mutable: bool,78		collection_admin: bool,79		token_owner: bool,80	) -> Result<()> {81		let caller = T::CrossAccountId::from_eth(caller);82		<Pallet<T>>::set_token_property_permissions(83			self,84			&caller,85			vec![PropertyKeyPermission {86				key: <Vec<u8>>::from(key)87					.try_into()88					.map_err(|_| "too long key")?,89				permission: PropertyPermission {90					mutable: is_mutable,91					collection_admin,92					token_owner,93				},94			}],95		)96		.map_err(dispatch_to_evm::<T>)97	}9899	/// @notice Set permissions for token property.100	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.101	/// @param permissions Permissions for keys.102	#[weight(<SelfWeightOf<T>>::set_token_property_permissions(permissions.len() as u32))]103	fn set_token_property_permissions(104		&mut self,105		caller: Caller,106		permissions: Vec<eth::TokenPropertyPermission>,107	) -> Result<()> {108		let caller = T::CrossAccountId::from_eth(caller);109		let perms = eth::TokenPropertyPermission::into_property_key_permissions(permissions)?;110111		<Pallet<T>>::set_token_property_permissions(self, &caller, perms)112			.map_err(dispatch_to_evm::<T>)113	}114115	/// @notice Get permissions for token properties.116	fn token_property_permissions(&self) -> Result<Vec<eth::TokenPropertyPermission>> {117		let perms = <Pallet<T>>::token_property_permission(self.id);118		Ok(perms119			.into_iter()120			.map(eth::TokenPropertyPermission::from)121			.collect())122	}123124	/// @notice Set token property value.125	/// @dev Throws error if `msg.sender` has no permission to edit the property.126	/// @param tokenId ID of the token.127	/// @param key Property key.128	/// @param value Property value.129	#[solidity(hide)]130	#[weight(<SelfWeightOf<T>>::set_token_properties(1))]131	fn set_property(132		&mut self,133		caller: Caller,134		token_id: U256,135		key: String,136		value: Bytes,137	) -> Result<()> {138		let caller = T::CrossAccountId::from_eth(caller);139		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;140		let key = <Vec<u8>>::from(key)141			.try_into()142			.map_err(|_| "key too long")?;143		let value = value.0.try_into().map_err(|_| "value too long")?;144145		let nesting_budget = self146			.recorder147			.weight_calls_budget(<StructureWeight<T>>::find_parent());148149		<Pallet<T>>::set_token_property(150			self,151			&caller,152			TokenId(token_id),153			Property { key, value },154			&nesting_budget,155		)156		.map_err(dispatch_to_evm::<T>)157	}158159	/// @notice Set token properties value.160	/// @dev Throws error if `msg.sender` has no permission to edit the property.161	/// @param tokenId ID of the token.162	/// @param properties settable properties163	#[weight(<SelfWeightOf<T>>::set_token_properties(properties.len() as u32))]164	fn set_properties(165		&mut self,166		caller: Caller,167		token_id: U256,168		properties: Vec<eth::Property>,169	) -> Result<()> {170		let caller = T::CrossAccountId::from_eth(caller);171		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;172173		let nesting_budget = self174			.recorder175			.weight_calls_budget(<StructureWeight<T>>::find_parent());176177		let properties = properties178			.into_iter()179			.map(eth::Property::try_into)180			.collect::<Result<Vec<_>>>()?;181182		<Pallet<T>>::set_token_properties(183			self,184			&caller,185			TokenId(token_id),186			properties.into_iter(),187			false,188			&nesting_budget,189		)190		.map_err(dispatch_to_evm::<T>)191	}192193	/// @notice Delete token property value.194	/// @dev Throws error if `msg.sender` has no permission to edit the property.195	/// @param tokenId ID of the token.196	/// @param key Property key.197	#[solidity(hide)]198	#[weight(<SelfWeightOf<T>>::delete_token_properties(1))]199	fn delete_property(&mut self, token_id: U256, caller: Caller, key: String) -> Result<()> {200		let caller = T::CrossAccountId::from_eth(caller);201		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;202		let key = <Vec<u8>>::from(key)203			.try_into()204			.map_err(|_| "key too long")?;205206		let nesting_budget = self207			.recorder208			.weight_calls_budget(<StructureWeight<T>>::find_parent());209210		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)211			.map_err(dispatch_to_evm::<T>)212	}213214	/// @notice Delete token properties value.215	/// @dev Throws error if `msg.sender` has no permission to edit the property.216	/// @param tokenId ID of the token.217	/// @param keys Properties key.218	#[weight(<SelfWeightOf<T>>::delete_token_properties(keys.len() as u32))]219	fn delete_properties(220		&mut self,221		token_id: U256,222		caller: Caller,223		keys: Vec<String>,224	) -> Result<()> {225		let caller = T::CrossAccountId::from_eth(caller);226		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;227		let keys = keys228			.into_iter()229			.map(|k| Ok(<Vec<u8>>::from(k).try_into().map_err(|_| "key too long")?))230			.collect::<Result<Vec<_>>>()?;231232		let nesting_budget = self233			.recorder234			.weight_calls_budget(<StructureWeight<T>>::find_parent());235236		<Pallet<T>>::delete_token_properties(237			self,238			&caller,239			TokenId(token_id),240			keys.into_iter(),241			&nesting_budget,242		)243		.map_err(dispatch_to_evm::<T>)244	}245246	/// @notice Get token property value.247	/// @dev Throws error if key not found248	/// @param tokenId ID of the token.249	/// @param key Property key.250	/// @return Property value bytes251	fn property(&self, token_id: U256, key: String) -> Result<Bytes> {252		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;253		let key = <Vec<u8>>::from(key)254			.try_into()255			.map_err(|_| "key too long")?;256257		let props = <TokenProperties<T>>::get((self.id, token_id));258		let prop = props.get(&key).ok_or("key not found")?;259260		Ok(prop.to_vec().into())261	}262}263264#[derive(ToLog)]265pub enum ERC721Events {266	/// @dev This event emits when NFTs are created (`from` == 0) and destroyed267	///  (`to` == 0). Exception: during contract creation, any number of RFTs268	///  may be created and assigned without emitting Transfer.269	Transfer {270		#[indexed]271		from: Address,272		#[indexed]273		to: Address,274		#[indexed]275		token_id: U256,276	},277	/// @dev Not supported278	Approval {279		#[indexed]280		owner: Address,281		#[indexed]282		approved: Address,283		#[indexed]284		token_id: U256,285	},286	/// @dev Not supported287	#[allow(dead_code)]288	ApprovalForAll {289		#[indexed]290		owner: Address,291		#[indexed]292		operator: Address,293		approved: bool,294	},295}296297/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension298/// @dev See https://eips.ethereum.org/EIPS/eip-721299#[solidity_interface(name = ERC721Metadata, enum(derive(PreDispatch)), expect_selector = 0x5b5e139f)]300impl<T: Config> RefungibleHandle<T>301where302	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,303{304	/// @notice A descriptive name for a collection of NFTs in this contract305	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`306	#[solidity(hide, rename_selector = "name")]307	fn name_proxy(&self) -> Result<String> {308		self.name()309	}310311	/// @notice An abbreviated name for NFTs in this contract312	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`313	#[solidity(hide, rename_selector = "symbol")]314	fn symbol_proxy(&self) -> Result<String> {315		self.symbol()316	}317318	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.319	///320	/// @dev If the token has a `url` property and it is not empty, it is returned.321	///  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`.322	///  If the collection property `baseURI` is empty or absent, return "" (empty string)323	///  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix324	///  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).325	///326	/// @return token's const_metadata327	#[solidity(rename_selector = "tokenURI")]328	fn token_uri(&self, token_id: U256) -> Result<String> {329		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;330331		match get_token_property(self, token_id_u32, &key::url()).as_deref() {332			Err(_) | Ok("") => (),333			Ok(url) => {334				return Ok(url.into());335			}336		};337338		let base_uri =339			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())340				.map(BoundedVec::into_inner)341				.map(String::from_utf8)342				.transpose()343				.map_err(|e| {344					Error::Revert(alloc::format!(345						"Can not convert value \"baseURI\" to string with error \"{}\"",346						e347					))348				})?;349350		let base_uri = match base_uri.as_deref() {351			None | Some("") => {352				return Ok("".into());353			}354			Some(base_uri) => base_uri.into(),355		};356357		Ok(358			match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {359				Err(_) | Ok("") => base_uri,360				Ok(suffix) => base_uri + suffix,361			},362		)363	}364}365366/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension367/// @dev See https://eips.ethereum.org/EIPS/eip-721368#[solidity_interface(name = ERC721Enumerable, enum(derive(PreDispatch)), expect_selector = 0x780e9d63)]369impl<T: Config> RefungibleHandle<T> {370	/// @notice Enumerate valid RFTs371	/// @param index A counter less than `totalSupply()`372	/// @return The token identifier for the `index`th NFT,373	///  (sort order not specified)374	fn token_by_index(&self, index: U256) -> U256 {375		index376	}377378	/// Not implemented379	fn token_of_owner_by_index(&self, _owner: Address, _index: U256) -> Result<U256> {380		// TODO: Not implemetable381		Err("not implemented".into())382	}383384	/// @notice Count RFTs tracked by this contract385	/// @return A count of valid RFTs tracked by this contract, where each one of386	///  them has an assigned and queryable owner not equal to the zero address387	fn total_supply(&self) -> Result<U256> {388		self.consume_store_reads(1)?;389		Ok(<Pallet<T>>::total_supply(self).into())390	}391}392393/// @title ERC-721 Non-Fungible Token Standard394/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md395#[solidity_interface(name = ERC721, events(ERC721Events), enum(derive(PreDispatch)), enum_attr(weight), expect_selector = 0x80ac58cd)]396impl<T: Config> RefungibleHandle<T> {397	/// @notice Count all RFTs assigned to an owner398	/// @dev RFTs assigned to the zero address are considered invalid, and this399	///  function throws for queries about the zero address.400	/// @param owner An address for whom to query the balance401	/// @return The number of RFTs owned by `owner`, possibly zero402	fn balance_of(&self, owner: Address) -> Result<U256> {403		self.consume_store_reads(1)?;404		let owner = T::CrossAccountId::from_eth(owner);405		let balance = <AccountBalance<T>>::get((self.id, owner));406		Ok(balance.into())407	}408409	/// @notice Find the owner of an RFT410	/// @dev RFTs assigned to zero address are considered invalid, and queries411	///  about them do throw.412	///  Returns special 0xffffffffffffffffffffffffffffffffffffffff address for413	///  the tokens that are partially owned.414	/// @param tokenId The identifier for an RFT415	/// @return The address of the owner of the RFT416	fn owner_of(&self, token_id: U256) -> Result<Address> {417		self.consume_store_reads(2)?;418		let token = token_id.try_into()?;419		let owner = <Pallet<T>>::token_owner(self.id, token);420		owner421			.map(|address| *address.as_eth())422			.or_else(|err| match err {423				TokenOwnerError::NotFound => Err(Error::Revert("token not found".into())),424				TokenOwnerError::MultipleOwners => Ok(ADDRESS_FOR_PARTIALLY_OWNED_TOKENS),425			})426	}427428	/// @dev Not implemented429	#[solidity(rename_selector = "safeTransferFrom")]430	fn safe_transfer_from_with_data(431		&mut self,432		_from: Address,433		_to: Address,434		_token_id: U256,435		_data: Bytes,436	) -> Result<()> {437		// TODO: Not implemetable438		Err("not implemented".into())439	}440441	/// @dev Not implemented442	#[solidity(rename_selector = "safeTransferFrom")]443	fn safe_transfer_from(&mut self, _from: Address, _to: Address, _token_id: U256) -> Result<()> {444		// TODO: Not implemetable445		Err("not implemented".into())446	}447448	/// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE449	///  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE450	///  THEY MAY BE PERMANENTLY LOST451	/// @dev Throws unless `msg.sender` is the current owner or an authorized452	///  operator for this RFT. Throws if `from` is not the current owner. Throws453	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.454	///  Throws if RFT pieces have multiple owners.455	/// @param from The current owner of the NFT456	/// @param to The new owner457	/// @param tokenId The NFT to transfer458	#[weight(<SelfWeightOf<T>>::transfer_from_creating_removing())]459	fn transfer_from(460		&mut self,461		caller: Caller,462		from: Address,463		to: Address,464		token_id: U256,465	) -> Result<()> {466		let caller = T::CrossAccountId::from_eth(caller);467		let from = T::CrossAccountId::from_eth(from);468		let to = T::CrossAccountId::from_eth(to);469		let token = token_id.try_into()?;470		let budget = self471			.recorder472			.weight_calls_budget(<StructureWeight<T>>::find_parent());473474		let balance = balance(&self, token, &from)?;475		ensure_single_owner(&self, token, balance)?;476477		<Pallet<T>>::transfer_from(self, &caller, &from, &to, token, balance, &budget)478			.map_err(dispatch_to_evm::<T>)?;479480		Ok(())481	}482483	/// @dev Not implemented484	fn approve(&mut self, _caller: Caller, _approved: Address, _token_id: U256) -> Result<()> {485		Err("not implemented".into())486	}487488	/// @notice Sets or unsets the approval of a given operator.489	///  The `operator` is allowed to transfer all token pieces of the `caller` on their behalf.490	/// @param operator Operator491	/// @param approved Should operator status be granted or revoked?492	#[weight(<SelfWeightOf<T>>::set_allowance_for_all())]493	fn set_approval_for_all(494		&mut self,495		caller: Caller,496		operator: Address,497		approved: bool,498	) -> Result<()> {499		let caller = T::CrossAccountId::from_eth(caller);500		let operator = T::CrossAccountId::from_eth(operator);501502		<Pallet<T>>::set_allowance_for_all(self, &caller, &operator, approved)503			.map_err(dispatch_to_evm::<T>)?;504		Ok(())505	}506507	/// @dev Not implemented508	fn get_approved(&self, _token_id: U256) -> Result<Address> {509		// TODO: Not implemetable510		Err("not implemented".into())511	}512513	/// @notice Tells whether the given `owner` approves the `operator`.514	#[weight(<SelfWeightOf<T>>::allowance_for_all())]515	fn is_approved_for_all(&self, owner: Address, operator: Address) -> Result<bool> {516		let owner = T::CrossAccountId::from_eth(owner);517		let operator = T::CrossAccountId::from_eth(operator);518519		Ok(<Pallet<T>>::allowance_for_all(self, &owner, &operator))520	}521}522523/// Returns amount of pieces of `token` that `owner` have524pub fn balance<T: Config>(525	collection: &RefungibleHandle<T>,526	token: TokenId,527	owner: &T::CrossAccountId,528) -> Result<u128> {529	collection.consume_store_reads(1)?;530	let balance = <Balance<T>>::get((collection.id, token, &owner));531	Ok(balance)532}533534/// Throws if `owner_balance` is lower than total amount of `token` pieces535pub fn ensure_single_owner<T: Config>(536	collection: &RefungibleHandle<T>,537	token: TokenId,538	owner_balance: u128,539) -> Result<()> {540	collection.consume_store_reads(1)?;541	let total_supply = <TotalSupply<T>>::get((collection.id, token));542543	if owner_balance == 0 {544		return Err(dispatch_to_evm::<T>(545			<CommonError<T>>::MustBeTokenOwner.into(),546		));547	}548549	if total_supply != owner_balance {550		return Err("token has multiple owners".into());551	}552	Ok(())553}554555/// @title ERC721 Token that can be irreversibly burned (destroyed).556#[solidity_interface(name = ERC721Burnable, enum(derive(PreDispatch)), enum_attr(weight))]557impl<T: Config> RefungibleHandle<T> {558	/// @notice Burns a specific ERC721 token.559	/// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized560	///  operator of the current owner.561	/// @param tokenId The RFT to approve562	#[weight(<SelfWeightOf<T>>::burn_item_fully())]563	fn burn(&mut self, caller: Caller, token_id: U256) -> Result<()> {564		let caller = T::CrossAccountId::from_eth(caller);565		let token = token_id.try_into()?;566567		let balance = balance(&self, token, &caller)?;568		ensure_single_owner(&self, token, balance)?;569570		<Pallet<T>>::burn(self, &caller, token, balance).map_err(dispatch_to_evm::<T>)?;571		Ok(())572	}573}574575/// @title ERC721 minting logic.576#[solidity_interface(name = ERC721UniqueMintable, enum(derive(PreDispatch)), enum_attr(weight))]577impl<T: Config> RefungibleHandle<T> {578	/// @notice Function to mint a token.579	/// @param to The new owner580	/// @return uint256 The id of the newly minted token581	#[weight(<SelfWeightOf<T>>::create_item())]582	fn mint(&mut self, caller: Caller, to: Address) -> Result<U256> {583		let token_id: U256 = <TokensMinted<T>>::get(self.id)584			.checked_add(1)585			.ok_or("item id overflow")?586			.into();587		self.mint_check_id(caller, to, token_id)?;588		Ok(token_id)589	}590591	/// @notice Function to mint a token.592	/// @dev `tokenId` should be obtained with `nextTokenId` method,593	///  unlike standard, you can't specify it manually594	/// @param to The new owner595	/// @param tokenId ID of the minted RFT596	#[solidity(hide, rename_selector = "mint")]597	#[weight(<SelfWeightOf<T>>::create_item())]598	fn mint_check_id(&mut self, caller: Caller, to: Address, token_id: U256) -> Result<bool> {599		let caller = T::CrossAccountId::from_eth(caller);600		let to = T::CrossAccountId::from_eth(to);601		let token_id: u32 = token_id.try_into()?;602		let budget = self603			.recorder604			.weight_calls_budget(<StructureWeight<T>>::find_parent());605606		if <TokensMinted<T>>::get(self.id)607			.checked_add(1)608			.ok_or("item id overflow")?609			!= token_id610		{611			return Err("item id should be next".into());612		}613614		let users = [(to.clone(), 1)]615			.into_iter()616			.collect::<BTreeMap<_, _>>()617			.try_into()618			.unwrap();619		<Pallet<T>>::create_item(620			self,621			&caller,622			CreateItemData::<T> {623				users,624				properties: CollectionPropertiesVec::default(),625			},626			&budget,627		)628		.map_err(dispatch_to_evm::<T>)?;629630		Ok(true)631	}632633	/// @notice Function to mint token with the given tokenUri.634	/// @param to The new owner635	/// @param tokenUri Token URI that would be stored in the NFT properties636	/// @return uint256 The id of the newly minted token637	#[solidity(rename_selector = "mintWithTokenURI")]638	#[weight(<SelfWeightOf<T>>::create_item() + <SelfWeightOf<T>>::set_token_properties(1))]639	fn mint_with_token_uri(640		&mut self,641		caller: Caller,642		to: Address,643		token_uri: String,644	) -> Result<U256> {645		let token_id: U256 = <TokensMinted<T>>::get(self.id)646			.checked_add(1)647			.ok_or("item id overflow")?648			.into();649		self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;650		Ok(token_id)651	}652653	/// @notice Function to mint token with the given tokenUri.654	/// @dev `tokenId` should be obtained with `nextTokenId` method,655	///  unlike standard, you can't specify it manually656	/// @param to The new owner657	/// @param tokenId ID of the minted RFT658	/// @param tokenUri Token URI that would be stored in the RFT properties659	#[solidity(hide, rename_selector = "mintWithTokenURI")]660	#[weight(<SelfWeightOf<T>>::create_item() + <SelfWeightOf<T>>::set_token_properties(1))]661	fn mint_with_token_uri_check_id(662		&mut self,663		caller: Caller,664		to: Address,665		token_id: U256,666		token_uri: String,667	) -> Result<bool> {668		let key = key::url();669		let permission = get_token_permission::<T>(self.id, &key)?;670		if !permission.collection_admin {671			return Err("Operation is not allowed".into());672		}673674		let caller = T::CrossAccountId::from_eth(caller);675		let to = T::CrossAccountId::from_eth(to);676		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;677		let budget = self678			.recorder679			.weight_calls_budget(<StructureWeight<T>>::find_parent());680681		if <TokensMinted<T>>::get(self.id)682			.checked_add(1)683			.ok_or("item id overflow")?684			!= token_id685		{686			return Err("item id should be next".into());687		}688689		let mut properties = CollectionPropertiesVec::default();690		properties691			.try_push(Property {692				key,693				value: token_uri694					.into_bytes()695					.try_into()696					.map_err(|_| "token uri is too long")?,697			})698			.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;699700		let users = [(to.clone(), 1)]701			.into_iter()702			.collect::<BTreeMap<_, _>>()703			.try_into()704			.unwrap();705		<Pallet<T>>::create_item(706			self,707			&caller,708			CreateItemData::<T> { users, properties },709			&budget,710		)711		.map_err(dispatch_to_evm::<T>)?;712		Ok(true)713	}714}715716fn get_token_property<T: Config>(717	collection: &CollectionHandle<T>,718	token_id: u32,719	key: &up_data_structs::PropertyKey,720) -> Result<String> {721	collection.consume_store_reads(1)?;722	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))723		.map_err(|_| Error::Revert("Token properties not found".into()))?;724	if let Some(property) = properties.get(key) {725		return Ok(String::from_utf8_lossy(property).into());726	}727728	Err("Property tokenURI not found".into())729}730731fn get_token_permission<T: Config>(732	collection_id: CollectionId,733	key: &PropertyKey,734) -> Result<PropertyPermission> {735	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)736		.map_err(|_| Error::Revert("No permissions for collection".into()))?;737	let a = token_property_permissions738		.get(key)739		.map(Clone::clone)740		.ok_or_else(|| {741			let key = String::from_utf8(key.clone().into_inner()).unwrap_or_default();742			Error::Revert(alloc::format!("No permission for key {}", key))743		})?;744	Ok(a)745}746747/// @title Unique extensions for ERC721.748#[solidity_interface(name = ERC721UniqueExtensions, enum(derive(PreDispatch)), enum_attr(weight))]749impl<T: Config> RefungibleHandle<T>750where751	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,752{753	/// @notice A descriptive name for a collection of NFTs in this contract754	fn name(&self) -> Result<String> {755		Ok(decode_utf16(self.name.iter().copied())756			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))757			.collect::<String>())758	}759760	/// @notice An abbreviated name for NFTs in this contract761	fn symbol(&self) -> Result<String> {762		Ok(String::from_utf8_lossy(&self.token_prefix).into())763	}764765	/// @notice A description for the collection.766	fn description(&self) -> Result<String> {767		Ok(decode_utf16(self.description.iter().copied())768			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))769			.collect::<String>())770	}771772	/// Returns the owner (in cross format) of the token.773	///774	/// @param tokenId Id for the token.775	fn cross_owner_of(&self, token_id: U256) -> Result<eth::CrossAddress> {776		Self::token_owner(&self, token_id.try_into()?)777			.map(|o| eth::CrossAddress::from_sub_cross_account::<T>(&o))778			.or_else(|err| match err {779				TokenOwnerError::NotFound => Err(Error::Revert("token not found".into())),780				TokenOwnerError::MultipleOwners => Ok(eth::CrossAddress::from_eth(781					ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,782				)),783			})784	}785786	/// Returns the token properties.787	///788	/// @param tokenId Id for the token.789	/// @param keys Properties keys. Empty keys for all propertyes.790	/// @return Vector of properties key/value pairs.791	fn properties(&self, token_id: U256, keys: Vec<String>) -> Result<Vec<eth::Property>> {792		let keys = keys793			.into_iter()794			.map(|key| {795				<Vec<u8>>::from(key)796					.try_into()797					.map_err(|_| Error::Revert("key too large".into()))798			})799			.collect::<Result<Vec<_>>>()?;800801		<Self as CommonCollectionOperations<T>>::token_properties(802			&self,803			token_id.try_into()?,804			if keys.is_empty() { None } else { Some(keys) },805		)806		.into_iter()807		.map(eth::Property::try_from)808		.collect::<Result<Vec<_>>>()809	}810	/// @notice Transfer ownership of an RFT811	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`812	///  is the zero address. Throws if `tokenId` is not a valid RFT.813	///  Throws if RFT pieces have multiple owners.814	/// @param to The new owner815	/// @param tokenId The RFT to transfer816	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]817	fn transfer(&mut self, caller: Caller, to: Address, token_id: U256) -> Result<()> {818		let caller = T::CrossAccountId::from_eth(caller);819		let to = T::CrossAccountId::from_eth(to);820		let token = token_id.try_into()?;821		let budget = self822			.recorder823			.weight_calls_budget(<StructureWeight<T>>::find_parent());824825		let balance = balance(self, token, &caller)?;826		ensure_single_owner(self, token, balance)?;827828		<Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)829			.map_err(dispatch_to_evm::<T>)?;830		Ok(())831	}832833	/// @notice Transfer ownership of an RFT834	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`835	///  is the zero address. Throws if `tokenId` is not a valid RFT.836	///  Throws if RFT pieces have multiple owners.837	/// @param to The new owner838	/// @param tokenId The RFT to transfer839	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]840	fn transfer_cross(841		&mut self,842		caller: Caller,843		to: eth::CrossAddress,844		token_id: U256,845	) -> Result<()> {846		let caller = T::CrossAccountId::from_eth(caller);847		let to = to.into_sub_cross_account::<T>()?;848		let token = token_id.try_into()?;849		let budget = self850			.recorder851			.weight_calls_budget(<StructureWeight<T>>::find_parent());852853		let balance = balance(self, token, &caller)?;854		ensure_single_owner(self, token, balance)?;855856		<Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)857			.map_err(dispatch_to_evm::<T>)?;858		Ok(())859	}860861	/// @notice Transfer ownership of an RFT862	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`863	///  is the zero address. Throws if `tokenId` is not a valid RFT.864	///  Throws if RFT pieces have multiple owners.865	/// @param to The new owner866	/// @param tokenId The RFT to transfer867	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]868	fn transfer_from_cross(869		&mut self,870		caller: Caller,871		from: eth::CrossAddress,872		to: eth::CrossAddress,873		token_id: U256,874	) -> Result<()> {875		let caller = T::CrossAccountId::from_eth(caller);876		let from = from.into_sub_cross_account::<T>()?;877		let to = to.into_sub_cross_account::<T>()?;878		let token_id = token_id.try_into()?;879		let budget = self880			.recorder881			.weight_calls_budget(<StructureWeight<T>>::find_parent());882883		let balance = balance(self, token_id, &from)?;884		ensure_single_owner(self, token_id, balance)?;885886		Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, balance, &budget)887			.map_err(dispatch_to_evm::<T>)?;888		Ok(())889	}890891	/// @notice Burns a specific ERC721 token.892	/// @dev Throws unless `msg.sender` is the current owner or an authorized893	///  operator for this RFT. Throws if `from` is not the current owner. Throws894	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.895	///  Throws if RFT pieces have multiple owners.896	/// @param from The current owner of the RFT897	/// @param tokenId The RFT to transfer898	#[solidity(hide)]899	#[weight(<SelfWeightOf<T>>::burn_from())]900	fn burn_from(&mut self, caller: Caller, from: Address, token_id: U256) -> Result<()> {901		let caller = T::CrossAccountId::from_eth(caller);902		let from = T::CrossAccountId::from_eth(from);903		let token = token_id.try_into()?;904		let budget = self905			.recorder906			.weight_calls_budget(<StructureWeight<T>>::find_parent());907908		let balance = balance(self, token, &from)?;909		ensure_single_owner(self, token, balance)?;910911		<Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)912			.map_err(dispatch_to_evm::<T>)?;913		Ok(())914	}915916	/// @notice Burns a specific ERC721 token.917	/// @dev Throws unless `msg.sender` is the current owner or an authorized918	///  operator for this RFT. Throws if `from` is not the current owner. Throws919	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.920	///  Throws if RFT pieces have multiple owners.921	/// @param from The current owner of the RFT922	/// @param tokenId The RFT to transfer923	#[weight(<SelfWeightOf<T>>::burn_from())]924	fn burn_from_cross(925		&mut self,926		caller: Caller,927		from: eth::CrossAddress,928		token_id: U256,929	) -> Result<()> {930		let caller = T::CrossAccountId::from_eth(caller);931		let from = from.into_sub_cross_account::<T>()?;932		let token = token_id.try_into()?;933		let budget = self934			.recorder935			.weight_calls_budget(<StructureWeight<T>>::find_parent());936937		let balance = balance(self, token, &from)?;938		ensure_single_owner(self, token, balance)?;939940		<Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)941			.map_err(dispatch_to_evm::<T>)?;942		Ok(())943	}944945	/// @notice Returns next free RFT ID.946	fn next_token_id(&self) -> Result<U256> {947		self.consume_store_reads(1)?;948		Ok(<TokensMinted<T>>::get(self.id)949			.checked_add(1)950			.ok_or("item id overflow")?951			.into())952	}953954	/// @notice Function to mint multiple tokens.955	/// @dev `tokenIds` should be an array of consecutive numbers and first number956	///  should be obtained with `nextTokenId` method957	/// @param to The new owner958	/// @param tokenIds IDs of the minted RFTs959	#[solidity(hide)]960	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]961	fn mint_bulk(&mut self, caller: Caller, to: Address, token_ids: Vec<U256>) -> Result<bool> {962		let caller = T::CrossAccountId::from_eth(caller);963		let to = T::CrossAccountId::from_eth(to);964		let mut expected_index = <TokensMinted<T>>::get(self.id)965			.checked_add(1)966			.ok_or("item id overflow")?;967		let budget = self968			.recorder969			.weight_calls_budget(<StructureWeight<T>>::find_parent());970971		let total_tokens = token_ids.len();972		for id in token_ids.into_iter() {973			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;974			if id != expected_index {975				return Err("item id should be next".into());976			}977			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;978		}979		let users = [(to.clone(), 1)]980			.into_iter()981			.collect::<BTreeMap<_, _>>()982			.try_into()983			.unwrap();984		let create_item_data = CreateItemData::<T> {985			users,986			properties: CollectionPropertiesVec::default(),987		};988		let data = (0..total_tokens)989			.map(|_| create_item_data.clone())990			.collect();991992		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)993			.map_err(dispatch_to_evm::<T>)?;994		Ok(true)995	}996997	/// @notice Function to mint multiple tokens with the given tokenUris.998	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive999	///  numbers and first number should be obtained with `nextTokenId` method1000	/// @param to The new owner1001	/// @param tokens array of pairs of token ID and token URI for minted tokens1002	#[solidity(hide, rename_selector = "mintBulkWithTokenURI")]1003	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32) + <SelfWeightOf<T>>::set_token_properties(tokens.len() as u32))]1004	fn mint_bulk_with_token_uri(1005		&mut self,1006		caller: Caller,1007		to: Address,1008		tokens: Vec<TokenUri>,1009	) -> Result<bool> {1010		let key = key::url();1011		let caller = T::CrossAccountId::from_eth(caller);1012		let to = T::CrossAccountId::from_eth(to);1013		let mut expected_index = <TokensMinted<T>>::get(self.id)1014			.checked_add(1)1015			.ok_or("item id overflow")?;1016		let budget = self1017			.recorder1018			.weight_calls_budget(<StructureWeight<T>>::find_parent());10191020		let mut data = Vec::with_capacity(tokens.len());1021		let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]1022			.into_iter()1023			.collect::<BTreeMap<_, _>>()1024			.try_into()1025			.unwrap();1026		for TokenUri { id, uri } in tokens {1027			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;1028			if id != expected_index {1029				return Err("item id should be next".into());1030			}1031			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;10321033			let mut properties = CollectionPropertiesVec::default();1034			properties1035				.try_push(Property {1036					key: key.clone(),1037					value: uri1038						.into_bytes()1039						.try_into()1040						.map_err(|_| "token uri is too long")?,1041				})1042				.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;10431044			let create_item_data = CreateItemData::<T> {1045				users: users.clone(),1046				properties,1047			};1048			data.push(create_item_data);1049		}10501051		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)1052			.map_err(dispatch_to_evm::<T>)?;1053		Ok(true)1054	}10551056	/// @notice Function to mint a token.1057	/// @param to The new owner crossAccountId1058	/// @param properties Properties of minted token1059	/// @return uint256 The id of the newly minted token1060	#[weight(<SelfWeightOf<T>>::create_item() + <SelfWeightOf<T>>::set_token_properties(properties.len() as u32))]1061	fn mint_cross(1062		&mut self,1063		caller: Caller,1064		to: eth::CrossAddress,1065		properties: Vec<eth::Property>,1066	) -> Result<U256> {1067		let token_id = <TokensMinted<T>>::get(self.id)1068			.checked_add(1)1069			.ok_or("item id overflow")?;10701071		let to = to.into_sub_cross_account::<T>()?;10721073		let properties = properties1074			.into_iter()1075			.map(eth::Property::try_into)1076			.collect::<Result<Vec<_>>>()?1077			.try_into()1078			.map_err(|_| Error::Revert(alloc::format!("too many properties")))?;10791080		let caller = T::CrossAccountId::from_eth(caller);10811082		let budget = self1083			.recorder1084			.weight_calls_budget(<StructureWeight<T>>::find_parent());10851086		let users = [(to, 1)]1087			.into_iter()1088			.collect::<BTreeMap<_, _>>()1089			.try_into()1090			.unwrap();1091		<Pallet<T>>::create_item(1092			self,1093			&caller,1094			CreateItemData::<T> { users, properties },1095			&budget,1096		)1097		.map_err(dispatch_to_evm::<T>)?;10981099		Ok(token_id.into())1100	}11011102	/// Returns EVM address for refungible token1103	///1104	/// @param token ID of the token1105	fn token_contract_address(&self, token: U256) -> Result<Address> {1106		Ok(T::EvmTokenAddressMapping::token_to_address(1107			self.id,1108			token.try_into().map_err(|_| "token id overflow")?,1109		))1110	}11111112	/// @notice Returns collection helper contract address1113	fn collection_helper_address(&self) -> Result<Address> {1114		Ok(T::ContractAddress::get())1115	}1116}11171118#[solidity_interface(1119	name = UniqueRefungible,1120	is(1121		ERC721,1122		ERC721Enumerable,1123		ERC721UniqueExtensions,1124		ERC721UniqueMintable,1125		ERC721Burnable,1126		ERC721Metadata(if(this.flags.erc721metadata)),1127		Collection(via(common_mut returns CollectionHandle<T>)),1128		TokenProperties,1129	),1130	enum(derive(PreDispatch)),1131)]1132impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}11331134// Not a tests, but code generators1135generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true);1136generate_stubgen!(gen_iface, UniqueRefungibleCall<()>, false);11371138impl<T: Config> CommonEvmHandler for RefungibleHandle<T>1139where1140	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,1141{1142	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");1143	fn call(1144		self,1145		handle: &mut impl PrecompileHandle,1146	) -> Option<pallet_common::erc::PrecompileResult> {1147		call::<T, UniqueRefungibleCall<T>, _, _>(handle, self)1148	}1149}
modifiedpallets/refungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -100,7 +100,7 @@
 	Event as CommonEvent, Pallet as PalletCommon,
 };
 use pallet_structure::Pallet as PalletStructure;
-use sp_core::H160;
+use sp_core::{Get, H160};
 use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};
 use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};
 use up_data_structs::{
@@ -571,6 +571,11 @@
 			stored_properties,
 			is_token_owner,
 			|properties| <TokenProperties<T>>::set((collection.id, token_id), properties),
+			erc::ERC721TokenEvent::TokenChanged {
+				collection_id: collection_id_to_address(collection.id),
+				token_id: token_id.into(),
+			}
+			.to_log(T::ContractAddress::get()),
 		)
 	}
 
modifiedpallets/refungible/src/stubs/UniqueRefungible.soldiffbeforeafterboth
--- a/pallets/refungible/src/stubs/UniqueRefungible.sol
+++ b/pallets/refungible/src/stubs/UniqueRefungible.sol
@@ -17,9 +17,14 @@
 	}
 }
 
+/// @dev inlined interface
+contract ERC721TokenEvent {
+	event TokenChanged(address indexed collectionId, uint256 tokenId);
+}
+
 /// @title A contract that allows to set and delete token properties and change token property permissions.
 /// @dev the ERC-165 identifier for this interface is 0xde0695c2
-contract TokenProperties is Dummy, ERC165 {
+contract TokenProperties is Dummy, ERC165, ERC721TokenEvent {
 	// /// @notice Set permissions for token property.
 	// /// @dev Throws error if `msg.sender` is not admin or owner of the collection.
 	// /// @param key Property key.
modifiedpallets/unique/src/eth/stubs/CollectionHelpers.soldiffbeforeafterboth
--- a/pallets/unique/src/eth/stubs/CollectionHelpers.sol
+++ b/pallets/unique/src/eth/stubs/CollectionHelpers.sol
@@ -22,7 +22,6 @@
 	event CollectionCreated(address indexed owner, address indexed collectionId);
 	event CollectionDestroyed(address indexed collectionId);
 	event CollectionChanged(address indexed collectionId);
-	event TokenChanged(address indexed collectionId, uint256 tokenId);
 }
 
 /// @title Contract, which allows users to operate with collections
modifiedtests/src/eth/abi/collectionHelpers.jsondiffbeforeafterboth
--- a/tests/src/eth/abi/collectionHelpers.json
+++ b/tests/src/eth/abi/collectionHelpers.json
@@ -45,25 +45,6 @@
     "type": "event"
   },
   {
-    "anonymous": false,
-    "inputs": [
-      {
-        "indexed": true,
-        "internalType": "address",
-        "name": "collectionId",
-        "type": "address"
-      },
-      {
-        "indexed": false,
-        "internalType": "uint256",
-        "name": "tokenId",
-        "type": "uint256"
-      }
-    ],
-    "name": "TokenChanged",
-    "type": "event"
-  },
-  {
     "inputs": [
       { "internalType": "uint32", "name": "collectionId", "type": "uint32" }
     ],
modifiedtests/src/eth/abi/nonFungible.jsondiffbeforeafterboth
--- a/tests/src/eth/abi/nonFungible.json
+++ b/tests/src/eth/abi/nonFungible.json
@@ -55,6 +55,25 @@
       {
         "indexed": true,
         "internalType": "address",
+        "name": "collectionId",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint256",
+        "name": "tokenId",
+        "type": "uint256"
+      }
+    ],
+    "name": "TokenChanged",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "address",
         "name": "from",
         "type": "address"
       },
modifiedtests/src/eth/abi/reFungible.jsondiffbeforeafterboth
--- a/tests/src/eth/abi/reFungible.json
+++ b/tests/src/eth/abi/reFungible.json
@@ -55,6 +55,25 @@
       {
         "indexed": true,
         "internalType": "address",
+        "name": "collectionId",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint256",
+        "name": "tokenId",
+        "type": "uint256"
+      }
+    ],
+    "name": "TokenChanged",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "address",
         "name": "from",
         "type": "address"
       },
modifiedtests/src/eth/api/CollectionHelpers.soldiffbeforeafterboth
--- a/tests/src/eth/api/CollectionHelpers.sol
+++ b/tests/src/eth/api/CollectionHelpers.sol
@@ -17,7 +17,6 @@
 	event CollectionCreated(address indexed owner, address indexed collectionId);
 	event CollectionDestroyed(address indexed collectionId);
 	event CollectionChanged(address indexed collectionId);
-	event TokenChanged(address indexed collectionId, uint256 tokenId);
 }
 
 /// @title Contract, which allows users to operate with collections
modifiedtests/src/eth/api/UniqueNFT.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueNFT.sol
+++ b/tests/src/eth/api/UniqueNFT.sol
@@ -12,9 +12,14 @@
 	function supportsInterface(bytes4 interfaceID) external view returns (bool);
 }
 
+/// @dev inlined interface
+interface ERC721TokenEvent {
+	event TokenChanged(address indexed collectionId, uint256 tokenId);
+}
+
 /// @title A contract that allows to set and delete token properties and change token property permissions.
 /// @dev the ERC-165 identifier for this interface is 0xde0695c2
-interface TokenProperties is Dummy, ERC165 {
+interface TokenProperties is Dummy, ERC165, ERC721TokenEvent {
 	// /// @notice Set permissions for token property.
 	// /// @dev Throws error if `msg.sender` is not admin or owner of the collection.
 	// /// @param key Property key.
modifiedtests/src/eth/api/UniqueRefungible.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueRefungible.sol
+++ b/tests/src/eth/api/UniqueRefungible.sol
@@ -12,9 +12,14 @@
 	function supportsInterface(bytes4 interfaceID) external view returns (bool);
 }
 
+/// @dev inlined interface
+interface ERC721TokenEvent {
+	event TokenChanged(address indexed collectionId, uint256 tokenId);
+}
+
 /// @title A contract that allows to set and delete token properties and change token property permissions.
 /// @dev the ERC-165 identifier for this interface is 0xde0695c2
-interface TokenProperties is Dummy, ERC165 {
+interface TokenProperties is Dummy, ERC165, ERC721TokenEvent {
 	// /// @notice Set permissions for token property.
 	// /// @dev Throws error if `msg.sender` is not admin or owner of the collection.
 	// /// @param key Property key.
modifiedtests/src/eth/events.test.tsdiffbeforeafterboth
--- a/tests/src/eth/events.test.ts
+++ b/tests/src/eth/events.test.ts
@@ -29,8 +29,10 @@
   });
 });
 
-function clearEvents(ethEvents: NormalizedEvent[], subEvents: IEvent[]) {
-  ethEvents.splice(0);
+function clearEvents(ethEvents: NormalizedEvent[] | null, subEvents: IEvent[]) {
+  if (ethEvents !== null) {
+    ethEvents.splice(0);
+  }
   subEvents.splice(0);
 }
 
@@ -374,7 +376,6 @@
   const owner = await helper.eth.createAccountWithBalance(donor);
   const {collectionAddress} = await helper.eth.createCollection(mode, owner, 'A', 'B', 'C');
   const collection = await helper.ethNativeContract.collection(collectionAddress, mode, owner);
-  const collectionHelper = await helper.ethNativeContract.collectionHelpers(owner);
   const result = await collection.methods.mint(owner).send({from: owner});
   const tokenId = result.events.Transfer.returnValues.tokenId;
   await collection.methods.setTokenPropertyPermissions([
@@ -384,38 +385,31 @@
       [TokenPermissionField.CollectionAdmin, true]],
     ],
   ]).send({from: owner});
-
 
-  const ethEvents: any = [];
-  collectionHelper.events.allEvents((_: any, event: any) => {
-    ethEvents.push(event);
-  });
   const {unsubscribe, collectedEvents: subEvents} = await helper.subscribeEvents([{section: 'common', names: ['TokenPropertySet', 'TokenPropertyDeleted']}]);
   {
-    await collection.methods.setProperties(tokenId, [{key: 'A', value: [1,2,3]}]).send({from: owner});
+    const result = await collection.methods.setProperties(tokenId, [{key: 'A', value: [1,2,3]}]).send({from: owner});
     await helper.wait.newBlocks(1);
-    expect(ethEvents).to.containSubset([
-      {
-        event: 'TokenChanged',
-        returnValues: {
-          collectionId: collectionAddress,
-        },
+    expect(result.events.TokenChanged).to.be.like({
+      event: 'TokenChanged',
+      returnValues: {
+        collectionId: collectionAddress,
+        tokenId: tokenId,
       },
-    ]);
+    });
     expect(subEvents).to.containSubset([{method: 'TokenPropertySet'}]);
-    clearEvents(ethEvents, subEvents);
+    clearEvents(null, subEvents);
   }
   {
-    await collection.methods.deleteProperties(tokenId, ['A']).send({from: owner});
+    const result = await collection.methods.deleteProperties(tokenId, ['A']).send({from: owner});
     await helper.wait.newBlocks(1);
-    expect(ethEvents).to.containSubset([
-      {
-        event: 'TokenChanged',
-        returnValues: {
-          collectionId: collectionAddress,
-        },
+    expect(result.events.TokenChanged).to.be.like({
+      event: 'TokenChanged',
+      returnValues: {
+        collectionId: collectionAddress,
+        tokenId: tokenId,
       },
-    ]);
+    });
     expect(subEvents).to.containSubset([{method: 'TokenPropertyDeleted'}]);
   }
   unsubscribe();