git.delta.rocks / unique-network / refs/commits / 10fad8556d5c

difftreelog

Merge branch 'develop' into feature/token_owners

Yaroslav Bolyukin2022-07-22parents: #789afa1 #c2a34ed.patch.diff
in: master

15 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6320,13 +6320,16 @@
 
 [[package]]
 name = "pallet-refungible"
-version = "0.1.1"
+version = "0.1.2"
 dependencies = [
+ "ethereum",
+ "evm-coder",
  "frame-benchmarking",
  "frame-support",
  "frame-system",
  "pallet-common",
  "pallet-evm",
+ "pallet-evm-coder-substrate",
  "pallet-structure",
  "parity-scale-codec 3.1.5",
  "scale-info",
modifiedMakefilediffbeforeafterboth
--- a/Makefile
+++ b/Makefile
@@ -12,6 +12,10 @@
 NONFUNGIBLE_EVM_STUBS=./pallets/nonfungible/src/stubs
 NONFUNGIBLE_EVM_ABI=./tests/src/eth/nonFungibleAbi.json
 
+REFUNGIBLE_EVM_STUBS=./pallets/refungible/src/stubs
+RENFUNGIBLE_EVM_ABI=./tests/src/eth/reFungibleAbi.json
+RENFUNGIBLE_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
 
@@ -21,7 +25,7 @@
 TESTS_API=./tests/src/eth/api/
 
 .PHONY: regenerate_solidity
-regenerate_solidity: UniqueFungible.sol UniqueNFT.sol ContractHelpers.sol CollectionHelpers.sol
+regenerate_solidity: UniqueFungible.sol UniqueNFT.sol UniqueRefungibleToken.sol ContractHelpers.sol CollectionHelpers.sol
 
 UniqueFungible.sol:
 	PACKAGE=pallet-fungible NAME=erc::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
@@ -30,6 +34,10 @@
 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
+	
+UniqueRefungibleToken.sol:
+	PACKAGE=pallet-refungible NAME=erc_token::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
+	PACKAGE=pallet-refungible NAME=erc_token::gen_impl OUTPUT=$(REFUNGIBLE_EVM_STUBS)/$@ ./.maintain/scripts/generate_sol.sh
 
 ContractHelpers.sol:
 	PACKAGE=pallet-evm-contract-helpers NAME=eth::contract_helpers_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
@@ -47,6 +55,10 @@
 	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
+
 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
@@ -55,7 +67,7 @@
 	INPUT=$(COLLECTION_HELPER_STUBS)/$< OUTPUT=$(COLLECTION_HELPER_STUBS)/CollectionHelpers.raw ./.maintain/scripts/compile_stub.sh
 	INPUT=$(COLLECTION_HELPER_STUBS)/$< OUTPUT=$(COLLECTION_HELPER_ABI) ./.maintain/scripts/generate_abi.sh
 
-evm_stubs: UniqueFungible UniqueNFT ContractHelpers CollectionHelpers
+evm_stubs: UniqueFungible UniqueNFT UniqueRefungibleToken ContractHelpers CollectionHelpers
 
 .PHONY: _bench
 _bench:
modifiedpallets/refungible/CHANGELOG.mddiffbeforeafterboth
--- a/pallets/refungible/CHANGELOG.md
+++ b/pallets/refungible/CHANGELOG.md
@@ -2,12 +2,17 @@
 
 All notable changes to this project will be documented in this file.
 
-## [0.1.1] - 2022-07-14
+## [v0.1.2] - 2022-07-14
 
-### Added
+### Other changes
 
- - Implementation of 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. 
+feat(refungible-pallet): add ERC-20 EVM API for RFT token pieces ([#413](https://github.com/UniqueNetwork/unique-chain/pull/413))
+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
+
+### Other changes
+
+- feat: RPC method `token_owners` returning 10 owners in no particular order.
 
- 
- 
\ No newline at end of file
+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.1.1"
+version = "0.1.2"
 license = "GPLv3"
 edition = "2021"
 
@@ -11,36 +11,43 @@
 version = '3.1.2'
 
 [dependencies]
-frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-sp-core = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-pallet-evm = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.24" }
+evm-coder = { default-features = false, path = '../../crates/evm-coder' }
+pallet-evm-coder-substrate = { default-features = false, path = '../../pallets/evm-coder-substrate' }
 pallet-common = { default-features = false, path = '../common' }
 pallet-structure = { default-features = false, path = '../structure' }
+struct-versioning = { path = "../../crates/struct-versioning" }
 up-data-structs = { default-features = false, path = '../../primitives/data-structs' }
+
+ethereum = { version = "0.12.0", default-features = false }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive",] }
+
 frame-benchmarking = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-scale-info = { version = "2.0.1", default-features = false, features = [
-    "derive",
-] }
-struct-versioning = { path = "../../crates/struct-versioning" }
+frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
+frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
+pallet-evm = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.24" }
+sp-core = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
+sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
+sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
 
 [features]
 default = ["std"]
 std = [
+    "ethereum/std",
+    "evm-coder/std",
+    'frame-benchmarking/std',
     "frame-support/std",
     "frame-system/std",
+    "pallet-common/std",
+    "pallet-evm/std",
+    "pallet-evm-coder-substrate/std",
+    "pallet-structure/std",
     "sp-runtime/std",
     "sp-std/std",
     "up-data-structs/std",
-    "pallet-common/std",
-    "pallet-structure/std",
-    'frame-benchmarking/std',
-    "pallet-evm/std",
 ]
 runtime-benchmarks = [
     'frame-benchmarking',
     'frame-support/runtime-benchmarks',
     'frame-system/runtime-benchmarks',
+    'up-data-structs/runtime-benchmarks',
 ]
modifiedpallets/refungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -14,34 +14,35 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-use up_data_structs::TokenId;
-use pallet_common::erc::CommonEvmHandler;
-use pallet_evm::PrecompileHandle;
+extern crate alloc;
+use evm_coder::{generate_stubgen, solidity_interface, types::*};
 
-use crate::{Config, RefungibleHandle};
+use pallet_common::{CollectionHandle, erc::CollectionCall, erc::CommonEvmHandler};
 
-impl<T: Config> CommonEvmHandler for RefungibleHandle<T> {
-	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");
+use pallet_evm::PrecompileHandle;
+use pallet_evm_coder_substrate::call;
 
-	fn call(
-		self,
-		_handle: &mut impl PrecompileHandle,
-	) -> Option<pallet_common::erc::PrecompileResult> {
-		// TODO: Implement RFT variant of ERC721
-		None
-	}
-}
+use crate::{Config, RefungibleHandle};
 
-pub struct RefungibleTokenHandle<T: Config>(pub RefungibleHandle<T>, pub TokenId);
+#[solidity_interface(
+	name = "UniqueRFT",
+	is(via("CollectionHandle<T>", common_mut, Collection),)
+)]
+impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> {}
 
-impl<T: Config> CommonEvmHandler for RefungibleTokenHandle<T> {
-	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungibleToken.raw");
+// Not a tests, but code generators
+generate_stubgen!(gen_impl, UniqueRFTCall<()>, true);
+generate_stubgen!(gen_iface, UniqueRFTCall<()>, false);
 
+impl<T: Config> CommonEvmHandler for RefungibleHandle<T>
+where
+	T::AccountId: From<[u8; 32]>,
+{
+	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");
 	fn call(
 		self,
-		_handle: &mut impl PrecompileHandle,
+		handle: &mut impl PrecompileHandle,
 	) -> Option<pallet_common::erc::PrecompileResult> {
-		// TODO: Implement RFT variant of ERC20
-		None
+		call::<T, UniqueRFTCall<T>, _, _>(handle, self)
 	}
 }
addedpallets/refungible/src/erc_token.rsdiffbeforeafterboth
--- /dev/null
+++ b/pallets/refungible/src/erc_token.rs
@@ -0,0 +1,257 @@
+// 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/>.
+
+//! # Refungible Pallet EVM API for token pieces
+//!
+//! Provides ERC-20 standart support implementation and EVM API for unique extensions for Refungible Pallet.
+//! Method implementations are mostly doing parameter conversion and calling Nonfungible Pallet methods.
+
+extern crate alloc;
+use core::{
+	char::{REPLACEMENT_CHARACTER, decode_utf16},
+	convert::TryInto,
+	ops::Deref,
+};
+use evm_coder::{ToLog, execution::*, generate_stubgen, solidity_interface, types::*, weight};
+use pallet_common::{
+	CommonWeightInfo,
+	erc::{CommonEvmHandler, PrecompileResult},
+};
+use pallet_evm::{account::CrossAccountId, PrecompileHandle};
+use pallet_evm_coder_substrate::{call, dispatch_to_evm, WithRecorder};
+use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};
+use sp_std::vec::Vec;
+use up_data_structs::TokenId;
+
+use crate::{
+	Allowance, Balance, common::CommonWeights, Config, Pallet, RefungibleHandle, SelfWeightOf,
+	weights::WeightInfo, TotalSupply,
+};
+
+pub struct RefungibleTokenHandle<T: Config>(pub RefungibleHandle<T>, pub TokenId);
+
+#[derive(ToLog)]
+pub enum ERC20Events {
+	/// @dev This event is emitted when the amount of tokens (value) is sent
+	/// from the from address to the to address. In the case of minting new
+	/// tokens, the transfer is usually from the 0 address while in the case
+	/// of burning tokens the transfer is to 0.
+	Transfer {
+		#[indexed]
+		from: address,
+		#[indexed]
+		to: address,
+		value: uint256,
+	},
+	/// @dev This event is emitted when the amount of tokens (value) is approved
+	/// by the owner to be used by the spender.
+	Approval {
+		#[indexed]
+		owner: address,
+		#[indexed]
+		spender: address,
+		value: uint256,
+	},
+}
+
+/// @title Standard ERC20 token
+///
+/// @dev Implementation of the basic standard token.
+/// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
+#[solidity_interface(name = "ERC20", events(ERC20Events))]
+impl<T: Config> RefungibleTokenHandle<T> {
+	/// @return the name of the token.
+	fn name(&self) -> Result<string> {
+		Ok(decode_utf16(self.name.iter().copied())
+			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))
+			.collect::<string>())
+	}
+
+	/// @return the symbol of the token.
+	fn symbol(&self) -> Result<string> {
+		Ok(string::from_utf8_lossy(&self.token_prefix).into())
+	}
+
+	/// @dev Total number of tokens in existence
+	fn total_supply(&self) -> Result<uint256> {
+		self.consume_store_reads(1)?;
+		Ok(<TotalSupply<T>>::get((self.id, self.1)).into())
+	}
+
+	/// @dev Not supported
+	fn decimals(&self) -> Result<uint8> {
+		// Decimals aren't supported for refungible tokens
+		Ok(0)
+	}
+
+	/// @dev Gets the balance of the specified address.
+	/// @param owner The address to query the balance of.
+	/// @return An uint256 representing the amount owned by the passed address.
+	fn balance_of(&self, owner: address) -> Result<uint256> {
+		self.consume_store_reads(1)?;
+		let owner = T::CrossAccountId::from_eth(owner);
+		let balance = <Balance<T>>::get((self.id, self.1, owner));
+		Ok(balance.into())
+	}
+
+	/// @dev Transfer token for a specified address
+	/// @param to The address to transfer to.
+	/// @param amount The amount to be transferred.
+	#[weight(<CommonWeights<T>>::transfer())]
+	fn transfer(&mut self, caller: caller, to: address, amount: uint256) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = T::CrossAccountId::from_eth(to);
+		let amount = amount.try_into().map_err(|_| "amount overflow")?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		<Pallet<T>>::transfer(self, &caller, &to, self.1, amount, &budget)
+			.map_err(|_| "transfer error")?;
+		Ok(true)
+	}
+
+	/// @dev Transfer tokens from one address to another
+	/// @param from address The address which you want to send tokens from
+	/// @param to address The address which you want to transfer to
+	/// @param amount uint256 the amount of tokens to be transferred
+	#[weight(<CommonWeights<T>>::transfer_from())]
+	fn transfer_from(
+		&mut self,
+		caller: caller,
+		from: address,
+		to: address,
+		amount: uint256,
+	) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let from = T::CrossAccountId::from_eth(from);
+		let to = T::CrossAccountId::from_eth(to);
+		let amount = amount.try_into().map_err(|_| "amount overflow")?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		<Pallet<T>>::transfer_from(self, &caller, &from, &to, self.1, amount, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+
+	/// @dev Approve the passed address to spend the specified amount of tokens on behalf of `msg.sender`.
+	/// Beware that changing an allowance with this method brings the risk that someone may use both the old
+	/// and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
+	/// race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
+	/// https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
+	/// @param spender The address which will spend the funds.
+	/// @param amount The amount of tokens to be spent.
+	#[weight(<SelfWeightOf<T>>::approve())]
+	fn approve(&mut self, caller: caller, spender: address, amount: uint256) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let spender = T::CrossAccountId::from_eth(spender);
+		let amount = amount.try_into().map_err(|_| "amount overflow")?;
+
+		<Pallet<T>>::set_allowance(self, &caller, &spender, self.1, amount)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+
+	/// @dev Function to check the amount of tokens that an owner allowed to a spender.
+	/// @param owner address The address which owns the funds.
+	/// @param spender address The address which will spend the funds.
+	/// @return A uint256 specifying the amount of tokens still available for the spender.
+	fn allowance(&self, owner: address, spender: address) -> Result<uint256> {
+		self.consume_store_reads(1)?;
+		let owner = T::CrossAccountId::from_eth(owner);
+		let spender = T::CrossAccountId::from_eth(spender);
+
+		Ok(<Allowance<T>>::get((self.id, self.1, owner, spender)).into())
+	}
+}
+
+#[solidity_interface(name = "ERC20UniqueExtensions")]
+impl<T: Config> RefungibleTokenHandle<T> {
+	/// @dev Function that burns an amount of the token of a given account,
+	/// deducting from the sender's allowance for said account.
+	/// @param from The account whose tokens will be burnt.
+	/// @param amount The amount that will be burnt.
+	#[weight(<SelfWeightOf<T>>::burn_from())]
+	fn burn_from(&mut self, caller: caller, from: address, amount: uint256) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let from = T::CrossAccountId::from_eth(from);
+		let amount = amount.try_into().map_err(|_| "amount overflow")?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		<Pallet<T>>::burn_from(self, &caller, &from, self.1, amount, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+
+	/// @dev Function that changes total amount of the tokens.
+	///  Throws if `msg.sender` doesn't owns all of the tokens.
+	/// @param amount New total amount of the tokens.
+	#[weight(<SelfWeightOf<T>>::repartition_item())]
+	fn repartition(&mut self, caller: caller, amount: uint256) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let amount = amount.try_into().map_err(|_| "amount overflow")?;
+
+		<Pallet<T>>::repartition(self, &caller, self.1, amount).map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+}
+
+impl<T: Config> RefungibleTokenHandle<T> {
+	pub fn into_inner(self) -> RefungibleHandle<T> {
+		self.0
+	}
+	pub fn common_mut(&mut self) -> &mut RefungibleHandle<T> {
+		&mut self.0
+	}
+}
+
+impl<T: Config> WithRecorder<T> for RefungibleTokenHandle<T> {
+	fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder<T> {
+		self.0.recorder()
+	}
+	fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder<T> {
+		self.0.into_recorder()
+	}
+}
+
+impl<T: Config> Deref for RefungibleTokenHandle<T> {
+	type Target = RefungibleHandle<T>;
+
+	fn deref(&self) -> &Self::Target {
+		&self.0
+	}
+}
+
+#[solidity_interface(name = "UniqueRefungibleToken", is(ERC20, ERC20UniqueExtensions,))]
+impl<T: Config> RefungibleTokenHandle<T> where T::AccountId: From<[u8; 32]> {}
+
+generate_stubgen!(gen_impl, UniqueRefungibleTokenCall<()>, true);
+generate_stubgen!(gen_iface, UniqueRefungibleTokenCall<()>, false);
+
+impl<T: Config> CommonEvmHandler for RefungibleTokenHandle<T>
+where
+	T::AccountId: From<[u8; 32]>,
+{
+	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungibleToken.raw");
+
+	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {
+		call::<T, UniqueRefungibleTokenCall<T>, _, _>(handle, self)
+	}
+}
modifiedpallets/refungible/src/lib.rsdiffbeforeafterboth
before · pallets/refungible/src/lib.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 Pallet18//!19//! The Refungible pallet provides functionality for handling refungible collections and tokens.20//!21//! - [`Config`]22//! - [`RefungibleHandle`]23//! - [`Pallet`]24//! - [`CommonWeights`]25//!26//! ## Overview27//!28//! The Refungible pallet provides functions for:29//!30//! - RFT collection creation and removal31//! - Minting and burning of RFT tokens32//! - Partition and repartition of RFT tokens33//! - Retrieving number of pieces of RFT token34//! - Retrieving account balances35//! - Transfering RFT token pieces36//! - Burning RFT token pieces37//! - Setting and checking allowance for RFT tokens38//!39//! ### Terminology40//!41//! - **RFT token:** Non fungible token that was partitioned to pieces. If an account owns all42//!   of the RFT token pieces than it owns the RFT token and can repartition it.43//!44//! - **RFT Collection:** A collection of RFT tokens. All RFT tokens are part of a collection.45//!   Each collection has its own settings and set of permissions.46//!47//! - **RFT token piece:** A fungible part of an RFT token.48//!49//! - **Balance:** RFT token pieces owned by an account50//!51//! - **Allowance:** Maximum number of RFT token pieces that one account is allowed to52//!   transfer from the balance of another account53//!54//! - **Burning:** The process of “deleting” a token from a collection or removing token pieces from55//!   an account balance.56//!57//! ### Implementations58//!59//! The Refungible pallet provides implementations for the following traits. If these traits provide60//! the functionality that you need, then you can avoid coupling with the Refungible pallet.61//!62//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight63//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing64//!   with collections65//! - [`RefungibleExtensions`](pallet_common::RefungibleExtensions): Functions specific for refungible66//!   collection67//!68//! ## Interface69//!70//! ### Dispatchable Functions71//!72//! - `init_collection` - Create RFT collection. RFT collection can be configured to allow or deny access for73//!   some accounts.74//! - `destroy_collection` - Destroy exising RFT collection. There should be no tokens in the collection.75//! - `burn` - Burn some amount of RFT token pieces owned by account. Burns the RFT token if no pieces left.76//! - `transfer` - Transfer some amount of RFT token pieces. Transfers should be enabled for RFT collection.77//!   Nests the RFT token if RFT token pieces are sent to another token.78//! - `create_item` - Mint RFT token in collection. Sender should have permission to mint tokens.79//! - `set_allowance` - Set allowance for another account to transfer balance from sender's account.80//! - `repartition` - Repartition token to selected number of pieces. Sender should own all existing pieces.81//!82//! ## Assumptions83//!84//! * Total number of pieces for one token shouldn't exceed `up_data_structs::MAX_REFUNGIBLE_PIECES`.85//! * Total number of tokens of all types shouldn't be greater than `up_data_structs::MAX_TOKEN_PREFIX_LENGTH`.86//! * Sender should be in collection's allow list to perform operations on tokens.8788#![cfg_attr(not(feature = "std"), no_std)]8990use frame_support::{ensure, fail, BoundedVec, transactional, storage::with_transaction};91use up_data_structs::{92	AccessMode, CollectionId, CustomDataLimit, MAX_REFUNGIBLE_PIECES, TokenId,93	CreateCollectionData, CreateRefungibleExData, mapping::TokenAddressMapping, budget::Budget,94	Property, PropertyScope, TrySetProperty, PropertyKey, PropertyValue, PropertyPermission,95	PropertyKeyPermission,96};97use pallet_evm::account::CrossAccountId;98use pallet_common::{99	Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,100	CommonCollectionOperations as _,101};102use pallet_structure::Pallet as PalletStructure;103use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};104use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};105use core::ops::Deref;106use codec::{Encode, Decode, MaxEncodedLen};107use scale_info::TypeInfo;108109pub use pallet::*;110#[cfg(feature = "runtime-benchmarks")]111pub mod benchmarking;112pub mod common;113pub mod erc;114pub mod weights;115pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;116117#[struct_versioning::versioned(version = 2, upper)]118#[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]119pub struct ItemData {120	pub const_data: BoundedVec<u8, CustomDataLimit>,121122	#[version(..2)]123	pub variable_data: BoundedVec<u8, CustomDataLimit>,124}125126#[frame_support::pallet]127pub mod pallet {128	use super::*;129	use frame_support::{130		Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key,131		traits::StorageVersion,132	};133	use frame_system::pallet_prelude::*;134	use up_data_structs::{CollectionId, TokenId};135	use super::weights::WeightInfo;136137	#[pallet::error]138	pub enum Error<T> {139		/// Not Refungible item data used to mint in Refungible collection.140		NotRefungibleDataUsedToMintFungibleCollectionToken,141		/// Maximum refungibility exceeded142		WrongRefungiblePieces,143		/// Refungible token can't be repartitioned by user who isn't owns all pieces144		RepartitionWhileNotOwningAllPieces,145		/// Refungible token can't nest other tokens146		RefungibleDisallowsNesting,147		/// Setting item properties is not allowed148		SettingPropertiesNotAllowed,149	}150151	#[pallet::config]152	pub trait Config:153		frame_system::Config + pallet_common::Config + pallet_structure::Config154	{155		type WeightInfo: WeightInfo;156	}157158	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);159160	#[pallet::pallet]161	#[pallet::storage_version(STORAGE_VERSION)]162	#[pallet::generate_store(pub(super) trait Store)]163	pub struct Pallet<T>(_);164165	/// Amount of tokens minted for collection166	#[pallet::storage]167	pub type TokensMinted<T: Config> =168		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;169170	/// Amount of burnt tokens for collection171	#[pallet::storage]172	pub type TokensBurnt<T: Config> =173		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;174175	/// Custom data serialized to bytes for token176	#[pallet::storage]177	pub type TokenData<T: Config> = StorageNMap<178		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),179		Value = ItemData,180		QueryKind = ValueQuery,181	>;182183	#[pallet::storage]184	#[pallet::getter(fn token_properties)]185	pub type TokenProperties<T: Config> = StorageNMap<186		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),187		Value = up_data_structs::Properties,188		QueryKind = ValueQuery,189		OnEmpty = up_data_structs::TokenProperties,190	>;191192	/// Total amount of pieces for token193	#[pallet::storage]194	pub type TotalSupply<T: Config> = StorageNMap<195		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),196		Value = u128,197		QueryKind = ValueQuery,198	>;199200	/// Used to enumerate tokens owned by account201	#[pallet::storage]202	pub type Owned<T: Config> = StorageNMap<203		Key = (204			Key<Twox64Concat, CollectionId>,205			Key<Blake2_128Concat, T::CrossAccountId>,206			Key<Twox64Concat, TokenId>,207		),208		Value = bool,209		QueryKind = ValueQuery,210	>;211212	/// Amount of tokens owned by account213	#[pallet::storage]214	pub type AccountBalance<T: Config> = StorageNMap<215		Key = (216			Key<Twox64Concat, CollectionId>,217			// Owner218			Key<Blake2_128Concat, T::CrossAccountId>,219		),220		Value = u32,221		QueryKind = ValueQuery,222	>;223224	/// Amount of token pieces owned by account225	#[pallet::storage]226	pub type Balance<T: Config> = StorageNMap<227		Key = (228			Key<Twox64Concat, CollectionId>,229			Key<Twox64Concat, TokenId>,230			// Owner231			Key<Blake2_128Concat, T::CrossAccountId>,232		),233		Value = u128,234		QueryKind = ValueQuery,235	>;236237	/// Allowance set by an owner for a spender for a token238	#[pallet::storage]239	pub type Allowance<T: Config> = StorageNMap<240		Key = (241			Key<Twox64Concat, CollectionId>,242			Key<Twox64Concat, TokenId>,243			// Owner244			Key<Blake2_128, T::CrossAccountId>,245			// Spender246			Key<Blake2_128Concat, T::CrossAccountId>,247		),248		Value = u128,249		QueryKind = ValueQuery,250	>;251252	#[pallet::hooks]253	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {254		fn on_runtime_upgrade() -> Weight {255			if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {256				<TokenData<T>>::translate_values::<ItemDataVersion1, _>(|v| {257					Some(<ItemDataVersion2>::from(v))258				})259			}260261			0262		}263	}264}265266pub struct RefungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);267impl<T: Config> RefungibleHandle<T> {268	pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {269		Self(inner)270	}271	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {272		self.0273	}274}275impl<T: Config> Deref for RefungibleHandle<T> {276	type Target = pallet_common::CollectionHandle<T>;277278	fn deref(&self) -> &Self::Target {279		&self.0280	}281}282283impl<T: Config> Pallet<T> {284	/// Get number of RFT tokens in collection285	pub fn total_supply(collection: &RefungibleHandle<T>) -> u32 {286		<TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)287	}288289	/// Check that RFT token exists290	///291	/// - `token`: Token ID.292	pub fn token_exists(collection: &RefungibleHandle<T>, token: TokenId) -> bool {293		<TotalSupply<T>>::contains_key((collection.id, token))294	}295296	pub fn set_scoped_token_property(297		collection_id: CollectionId,298		token_id: TokenId,299		scope: PropertyScope,300		property: Property,301	) -> DispatchResult {302		TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {303			properties.try_scoped_set(scope, property.key, property.value)304		})305		.map_err(<CommonError<T>>::from)?;306307		Ok(())308	}309310	pub fn set_scoped_token_properties(311		collection_id: CollectionId,312		token_id: TokenId,313		scope: PropertyScope,314		properties: impl Iterator<Item = Property>,315	) -> DispatchResult {316		TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {317			stored_properties.try_scoped_set_from_iter(scope, properties)318		})319		.map_err(<CommonError<T>>::from)?;320321		Ok(())322	}323}324325// unchecked calls skips any permission checks326impl<T: Config> Pallet<T> {327	/// Create RFT collection328	///329	/// `init_collection` will take non-refundable deposit for collection creation.330	///331	/// - `data`: Contains settings for collection limits and permissions.332	pub fn init_collection(333		owner: T::CrossAccountId,334		data: CreateCollectionData<T::AccountId>,335	) -> Result<CollectionId, DispatchError> {336		<PalletCommon<T>>::init_collection(owner, data, false)337	}338339	/// Destroy RFT collection340	///341	/// `destroy_collection` will throw error if collection contains any tokens.342	/// Only owner can destroy collection.343	pub fn destroy_collection(344		collection: RefungibleHandle<T>,345		sender: &T::CrossAccountId,346	) -> DispatchResult {347		let id = collection.id;348349		if Self::collection_has_tokens(id) {350			return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());351		}352353		// =========354355		PalletCommon::destroy_collection(collection.0, sender)?;356357		<TokensMinted<T>>::remove(id);358		<TokensBurnt<T>>::remove(id);359		<TokenData<T>>::remove_prefix((id,), None);360		<TotalSupply<T>>::remove_prefix((id,), None);361		<Balance<T>>::remove_prefix((id,), None);362		<Allowance<T>>::remove_prefix((id,), None);363		<Owned<T>>::remove_prefix((id,), None);364		<AccountBalance<T>>::remove_prefix((id,), None);365		Ok(())366	}367368	fn collection_has_tokens(collection_id: CollectionId) -> bool {369		<TokenData<T>>::iter_prefix((collection_id,))370			.next()371			.is_some()372	}373374	pub fn burn_token_unchecked(375		collection: &RefungibleHandle<T>,376		token_id: TokenId,377	) -> DispatchResult {378		let burnt = <TokensBurnt<T>>::get(collection.id)379			.checked_add(1)380			.ok_or(ArithmeticError::Overflow)?;381382		<TokensBurnt<T>>::insert(collection.id, burnt);383		<TokenData<T>>::remove((collection.id, token_id));384		<TokenProperties<T>>::remove((collection.id, token_id));385		<TotalSupply<T>>::remove((collection.id, token_id));386		<Balance<T>>::remove_prefix((collection.id, token_id), None);387		<Allowance<T>>::remove_prefix((collection.id, token_id), None);388		// TODO: ERC721 transfer event389		Ok(())390	}391392	/// Burn RFT token pieces393	///394	/// `burn` will decrease total amount of token pieces and amount owned by sender.395	/// `burn` can be called even if there are multiple owners of the RFT token.396	/// If sender wouldn't have any pieces left after `burn` than she will stop being397	/// one of the owners of the token. If there is no account that owns any pieces of398	/// the token than token will be burned too.399	///400	/// - `amount`: Amount of token pieces to burn.401	/// - `token`: Token who's pieces should be burned402	/// - `collection`: Collection that contains the token403	pub fn burn(404		collection: &RefungibleHandle<T>,405		owner: &T::CrossAccountId,406		token: TokenId,407		amount: u128,408	) -> DispatchResult {409		let total_supply = <TotalSupply<T>>::get((collection.id, token))410			.checked_sub(amount)411			.ok_or(<CommonError<T>>::TokenValueTooLow)?;412413		// This was probally last owner of this token?414		if total_supply == 0 {415			// Ensure user actually owns this amount416			ensure!(417				<Balance<T>>::get((collection.id, token, owner)) == amount,418				<CommonError<T>>::TokenValueTooLow419			);420			let account_balance = <AccountBalance<T>>::get((collection.id, owner))421				.checked_sub(1)422				// Should not occur423				.ok_or(ArithmeticError::Underflow)?;424425			// =========426427			<Owned<T>>::remove((collection.id, owner, token));428			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);429			<AccountBalance<T>>::insert((collection.id, owner), account_balance);430			Self::burn_token_unchecked(collection, token)?;431			<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(432				collection.id,433				token,434				owner.clone(),435				amount,436			));437			return Ok(());438		}439440		let balance = <Balance<T>>::get((collection.id, token, owner))441			.checked_sub(amount)442			.ok_or(<CommonError<T>>::TokenValueTooLow)?;443		let account_balance = if balance == 0 {444			<AccountBalance<T>>::get((collection.id, owner))445				.checked_sub(1)446				// Should not occur447				.ok_or(ArithmeticError::Underflow)?448		} else {449			0450		};451452		// =========453454		if balance == 0 {455			<Owned<T>>::remove((collection.id, owner, token));456			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);457			<Balance<T>>::remove((collection.id, token, owner));458			<AccountBalance<T>>::insert((collection.id, owner), account_balance);459		} else {460			<Balance<T>>::insert((collection.id, token, owner), balance);461		}462		<TotalSupply<T>>::insert((collection.id, token), total_supply);463		// TODO: ERC20 transfer event464		<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(465			collection.id,466			token,467			owner.clone(),468			amount,469		));470		Ok(())471	}472473	#[transactional]474	fn modify_token_properties(475		collection: &RefungibleHandle<T>,476		sender: &T::CrossAccountId,477		token_id: TokenId,478		properties: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,479		is_token_create: bool,480		nesting_budget: &dyn Budget,481	) -> DispatchResult {482		let is_collection_admin = || collection.is_owner_or_admin(sender);483		let is_token_owner = || -> Result<bool, DispatchError> {484			let balance = collection.balance(sender.clone(), token_id);485			let total_pieces: u128 =486				Self::total_pieces(collection.id, token_id).unwrap_or(u128::MAX);487			if balance != total_pieces {488				return Ok(false);489			}490491			let is_bundle_owner = <PalletStructure<T>>::check_indirectly_owned(492				sender.clone(),493				collection.id,494				token_id,495				None,496				nesting_budget,497			)?;498499			Ok(is_bundle_owner)500		};501502		for (key, value) in properties {503			let permission = <PalletCommon<T>>::property_permissions(collection.id)504				.get(&key)505				.cloned()506				.unwrap_or_else(PropertyPermission::none);507508			let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))509				.get(&key)510				.is_some();511512			match permission {513				PropertyPermission { mutable: false, .. } if is_property_exists => {514					return Err(<CommonError<T>>::NoPermission.into());515				}516517				PropertyPermission {518					collection_admin,519					token_owner,520					..521				} => {522					//TODO: investigate threats during public minting.523					let is_token_create =524						is_token_create && (collection_admin || token_owner) && value.is_some();525					if !(is_token_create526						|| (collection_admin && is_collection_admin())527						|| (token_owner && is_token_owner()?))528					{529						fail!(<CommonError<T>>::NoPermission);530					}531				}532			}533534			match value {535				Some(value) => {536					<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {537						properties.try_set(key.clone(), value)538					})539					.map_err(<CommonError<T>>::from)?;540541					<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(542						collection.id,543						token_id,544						key,545					));546				}547				None => {548					<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {549						properties.remove(&key)550					})551					.map_err(<CommonError<T>>::from)?;552553					<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(554						collection.id,555						token_id,556						key,557					));558				}559			}560		}561562		Ok(())563	}564565	pub fn set_token_properties(566		collection: &RefungibleHandle<T>,567		sender: &T::CrossAccountId,568		token_id: TokenId,569		properties: impl Iterator<Item = Property>,570		is_token_create: bool,571		nesting_budget: &dyn Budget,572	) -> DispatchResult {573		Self::modify_token_properties(574			collection,575			sender,576			token_id,577			properties.map(|p| (p.key, Some(p.value))),578			is_token_create,579			nesting_budget,580		)581	}582583	pub fn set_token_property(584		collection: &RefungibleHandle<T>,585		sender: &T::CrossAccountId,586		token_id: TokenId,587		property: Property,588		nesting_budget: &dyn Budget,589	) -> DispatchResult {590		let is_token_create = false;591592		Self::set_token_properties(593			collection,594			sender,595			token_id,596			[property].into_iter(),597			is_token_create,598			nesting_budget,599		)600	}601602	pub fn delete_token_properties(603		collection: &RefungibleHandle<T>,604		sender: &T::CrossAccountId,605		token_id: TokenId,606		property_keys: impl Iterator<Item = PropertyKey>,607		nesting_budget: &dyn Budget,608	) -> DispatchResult {609		let is_token_create = false;610611		Self::modify_token_properties(612			collection,613			sender,614			token_id,615			property_keys.into_iter().map(|key| (key, None)),616			is_token_create,617			nesting_budget,618		)619	}620621	pub fn delete_token_property(622		collection: &RefungibleHandle<T>,623		sender: &T::CrossAccountId,624		token_id: TokenId,625		property_key: PropertyKey,626		nesting_budget: &dyn Budget,627	) -> DispatchResult {628		Self::delete_token_properties(629			collection,630			sender,631			token_id,632			[property_key].into_iter(),633			nesting_budget,634		)635	}636637	/// Transfer RFT token pieces from one account to another.638	///639	/// If the sender is no longer owns any pieces after the `transfer` than she stops being an owner of the token.640	///641	/// - `from`: Owner of token pieces to transfer.642	/// - `to`: Recepient of transfered token pieces.643	/// - `amount`: Amount of token pieces to transfer.644	/// - `token`: Token whos pieces should be transfered645	/// - `collection`: Collection that contains the token646	pub fn transfer(647		collection: &RefungibleHandle<T>,648		from: &T::CrossAccountId,649		to: &T::CrossAccountId,650		token: TokenId,651		amount: u128,652		nesting_budget: &dyn Budget,653	) -> DispatchResult {654		ensure!(655			collection.limits.transfers_enabled(),656			<CommonError<T>>::TransferNotAllowed657		);658659		if collection.permissions.access() == AccessMode::AllowList {660			collection.check_allowlist(from)?;661			collection.check_allowlist(to)?;662		}663		<PalletCommon<T>>::ensure_correct_receiver(to)?;664665		let balance_from = <Balance<T>>::get((collection.id, token, from))666			.checked_sub(amount)667			.ok_or(<CommonError<T>>::TokenValueTooLow)?;668		let mut create_target = false;669		let from_to_differ = from != to;670		let balance_to = if from != to {671			let old_balance = <Balance<T>>::get((collection.id, token, to));672			if old_balance == 0 {673				create_target = true;674			}675			Some(676				old_balance677					.checked_add(amount)678					.ok_or(ArithmeticError::Overflow)?,679			)680		} else {681			None682		};683684		let account_balance_from = if balance_from == 0 {685			Some(686				<AccountBalance<T>>::get((collection.id, from))687					.checked_sub(1)688					// Should not occur689					.ok_or(ArithmeticError::Underflow)?,690			)691		} else {692			None693		};694		// Account data is created in token, AccountBalance should be increased695		// But only if from != to as we shouldn't check overflow in this case696		let account_balance_to = if create_target && from_to_differ {697			let account_balance_to = <AccountBalance<T>>::get((collection.id, to))698				.checked_add(1)699				.ok_or(ArithmeticError::Overflow)?;700			ensure!(701				account_balance_to < collection.limits.account_token_ownership_limit(),702				<CommonError<T>>::AccountTokenLimitExceeded,703			);704705			Some(account_balance_to)706		} else {707			None708		};709710		// =========711712		<PalletStructure<T>>::nest_if_sent_to_token(713			from.clone(),714			to,715			collection.id,716			token,717			nesting_budget,718		)?;719720		if let Some(balance_to) = balance_to {721			// from != to722			if balance_from == 0 {723				<Balance<T>>::remove((collection.id, token, from));724				<PalletStructure<T>>::unnest_if_nested(from, collection.id, token);725			} else {726				<Balance<T>>::insert((collection.id, token, from), balance_from);727			}728			<Balance<T>>::insert((collection.id, token, to), balance_to);729			if let Some(account_balance_from) = account_balance_from {730				<AccountBalance<T>>::insert((collection.id, from), account_balance_from);731				<Owned<T>>::remove((collection.id, from, token));732			}733			if let Some(account_balance_to) = account_balance_to {734				<AccountBalance<T>>::insert((collection.id, to), account_balance_to);735				<Owned<T>>::insert((collection.id, to, token), true);736			}737		}738739		// TODO: ERC20 transfer event740		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(741			collection.id,742			token,743			from.clone(),744			to.clone(),745			amount,746		));747		Ok(())748	}749750	/// Batched operation to create multiple RFT tokens.751	///752	/// Same as `create_item` but creates multiple tokens.753	///754	/// - `data`: Same as 'data` in `create_item` but contains data for multiple tokens.755	pub fn create_multiple_items(756		collection: &RefungibleHandle<T>,757		sender: &T::CrossAccountId,758		data: Vec<CreateRefungibleExData<T::CrossAccountId>>,759		nesting_budget: &dyn Budget,760	) -> DispatchResult {761		if !collection.is_owner_or_admin(sender) {762			ensure!(763				collection.permissions.mint_mode(),764				<CommonError<T>>::PublicMintingNotAllowed765			);766			collection.check_allowlist(sender)?;767768			for item in data.iter() {769				for user in item.users.keys() {770					collection.check_allowlist(user)?;771				}772			}773		}774775		for item in data.iter() {776			for (owner, _) in item.users.iter() {777				<PalletCommon<T>>::ensure_correct_receiver(owner)?;778			}779		}780781		// Total pieces per tokens782		let totals = data783			.iter()784			.map(|data| {785				Ok(data786					.users787					.iter()788					.map(|u| u.1)789					.try_fold(0u128, |acc, v| acc.checked_add(*v))790					.ok_or(ArithmeticError::Overflow)?)791			})792			.collect::<Result<Vec<_>, DispatchError>>()?;793		for total in &totals {794			ensure!(795				*total <= MAX_REFUNGIBLE_PIECES,796				<Error<T>>::WrongRefungiblePieces797			);798		}799800		let first_token_id = <TokensMinted<T>>::get(collection.id);801		let tokens_minted = first_token_id802			.checked_add(data.len() as u32)803			.ok_or(ArithmeticError::Overflow)?;804		ensure!(805			tokens_minted < collection.limits.token_limit(),806			<CommonError<T>>::CollectionTokenLimitExceeded807		);808809		let mut balances = BTreeMap::new();810		for data in &data {811			for owner in data.users.keys() {812				let balance = balances813					.entry(owner)814					.or_insert_with(|| <AccountBalance<T>>::get((collection.id, owner)));815				*balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;816817				ensure!(818					*balance <= collection.limits.account_token_ownership_limit(),819					<CommonError<T>>::AccountTokenLimitExceeded,820				);821			}822		}823824		for (i, token) in data.iter().enumerate() {825			let token_id = TokenId(first_token_id + i as u32 + 1);826			for (to, _) in token.users.iter() {827				<PalletStructure<T>>::check_nesting(828					sender.clone(),829					to,830					collection.id,831					token_id,832					nesting_budget,833				)?;834			}835		}836837		// =========838839		with_transaction(|| {840			for (i, data) in data.iter().enumerate() {841				let token_id = first_token_id + i as u32 + 1;842				<TotalSupply<T>>::insert((collection.id, token_id), totals[i]);843844				<TokenData<T>>::insert(845					(collection.id, token_id),846					ItemData {847						const_data: data.const_data.clone(),848					},849				);850851				for (user, amount) in data.users.iter() {852					if *amount == 0 {853						continue;854					}855					<Balance<T>>::insert((collection.id, token_id, &user), amount);856					<Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);857					<PalletStructure<T>>::nest_if_sent_to_token_unchecked(858						user,859						collection.id,860						TokenId(token_id),861					);862				}863864				if let Err(e) = Self::set_token_properties(865					collection,866					sender,867					TokenId(token_id),868					data.properties.clone().into_iter(),869					true,870					nesting_budget,871				) {872					return TransactionOutcome::Rollback(Err(e));873				}874			}875			TransactionOutcome::Commit(Ok(()))876		})?;877878		<TokensMinted<T>>::insert(collection.id, tokens_minted);879880		for (account, balance) in balances {881			<AccountBalance<T>>::insert((collection.id, account), balance);882		}883884		for (i, token) in data.into_iter().enumerate() {885			let token_id = first_token_id + i as u32 + 1;886887			for (user, amount) in token.users.into_iter() {888				if amount == 0 {889					continue;890				}891892				// TODO: ERC20 transfer event893				<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(894					collection.id,895					TokenId(token_id),896					user,897					amount,898				));899			}900		}901		Ok(())902	}903904	pub fn set_allowance_unchecked(905		collection: &RefungibleHandle<T>,906		sender: &T::CrossAccountId,907		spender: &T::CrossAccountId,908		token: TokenId,909		amount: u128,910	) {911		if amount == 0 {912			<Allowance<T>>::remove((collection.id, token, sender, spender));913		} else {914			<Allowance<T>>::insert((collection.id, token, sender, spender), amount);915		}916		// TODO: ERC20 approval event917		<PalletCommon<T>>::deposit_event(CommonEvent::Approved(918			collection.id,919			token,920			sender.clone(),921			spender.clone(),922			amount,923		))924	}925926	/// Set allowance for the spender to `transfer` or `burn` sender's token pieces.927	///928	/// - `amount`: Amount of token pieces the spender is allowed to `transfer` or `burn.929	pub fn set_allowance(930		collection: &RefungibleHandle<T>,931		sender: &T::CrossAccountId,932		spender: &T::CrossAccountId,933		token: TokenId,934		amount: u128,935	) -> DispatchResult {936		if collection.permissions.access() == AccessMode::AllowList {937			collection.check_allowlist(sender)?;938			collection.check_allowlist(spender)?;939		}940941		<PalletCommon<T>>::ensure_correct_receiver(spender)?;942943		if <Balance<T>>::get((collection.id, token, sender)) < amount {944			ensure!(945				collection.ignores_owned_amount(sender) && Self::token_exists(collection, token),946				<CommonError<T>>::CantApproveMoreThanOwned947			);948		}949950		// =========951952		Self::set_allowance_unchecked(collection, sender, spender, token, amount);953		Ok(())954	}955956	/// Returns allowance, which should be set after transaction957	fn check_allowed(958		collection: &RefungibleHandle<T>,959		spender: &T::CrossAccountId,960		from: &T::CrossAccountId,961		token: TokenId,962		amount: u128,963		nesting_budget: &dyn Budget,964	) -> Result<Option<u128>, DispatchError> {965		if spender.conv_eq(from) {966			return Ok(None);967		}968		if collection.permissions.access() == AccessMode::AllowList {969			// `from`, `to` checked in [`transfer`]970			collection.check_allowlist(spender)?;971		}972		if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {973			// TODO: should collection owner be allowed to perform this transfer?974			ensure!(975				<PalletStructure<T>>::check_indirectly_owned(976					spender.clone(),977					source.0,978					source.1,979					None,980					nesting_budget981				)?,982				<CommonError<T>>::ApprovedValueTooLow,983			);984			return Ok(None);985		}986		let allowance =987			<Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);988		if allowance.is_none() {989			ensure!(990				collection.ignores_allowance(spender),991				<CommonError<T>>::ApprovedValueTooLow992			);993		}994		Ok(allowance)995	}996997	/// Transfer RFT token pieces from one account to another.998	///999	/// Same as the [`transfer`] but spender doesn't needs to be an owner of the token pieces.1000	/// The owner should set allowance for the spender to transfer pieces.1001	///1002	/// [`transfer`]: struct.Pallet.html#method.transfer1003	pub fn transfer_from(1004		collection: &RefungibleHandle<T>,1005		spender: &T::CrossAccountId,1006		from: &T::CrossAccountId,1007		to: &T::CrossAccountId,1008		token: TokenId,1009		amount: u128,1010		nesting_budget: &dyn Budget,1011	) -> DispatchResult {1012		let allowance =1013			Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;10141015		// =========10161017		Self::transfer(collection, from, to, token, amount, nesting_budget)?;1018		if let Some(allowance) = allowance {1019			Self::set_allowance_unchecked(collection, from, spender, token, allowance);1020		}1021		Ok(())1022	}10231024	/// Burn RFT token pieces from the account.1025	///1026	/// Same as the [`burn`] but spender doesn't need to be an owner of the token pieces. The owner should1027	/// set allowance for the spender to burn pieces1028	///1029	/// [`burn`]: struct.Pallet.html#method.burn1030	pub fn burn_from(1031		collection: &RefungibleHandle<T>,1032		spender: &T::CrossAccountId,1033		from: &T::CrossAccountId,1034		token: TokenId,1035		amount: u128,1036		nesting_budget: &dyn Budget,1037	) -> DispatchResult {1038		let allowance =1039			Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;10401041		// =========10421043		Self::burn(collection, from, token, amount)?;1044		if let Some(allowance) = allowance {1045			Self::set_allowance_unchecked(collection, from, spender, token, allowance);1046		}1047		Ok(())1048	}10491050	/// Create RFT token.1051	///1052	/// The sender should be the owner/admin of the collection or collection should be configured1053	/// to allow public minting.1054	///1055	/// - `data`: Contains list of users who will become the owners of the token pieces and amount1056	///   of token pieces they will receive.1057	pub fn create_item(1058		collection: &RefungibleHandle<T>,1059		sender: &T::CrossAccountId,1060		data: CreateRefungibleExData<T::CrossAccountId>,1061		nesting_budget: &dyn Budget,1062	) -> DispatchResult {1063		Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1064	}10651066	/// Repartition RFT token.1067	///1068	/// `repartition` will set token balance of the sender and total amount of token pieces.1069	/// Sender should own all of the token pieces. `repartition' could be done even if some1070	/// token pieces were burned before.1071	///1072	/// - `amount`: Total amount of token pieces that the token will have after `repartition`.1073	pub fn repartition(1074		collection: &RefungibleHandle<T>,1075		owner: &T::CrossAccountId,1076		token: TokenId,1077		amount: u128,1078	) -> DispatchResult {1079		ensure!(1080			amount <= MAX_REFUNGIBLE_PIECES,1081			<Error<T>>::WrongRefungiblePieces1082		);1083		ensure!(amount > 0, <CommonError<T>>::TokenValueTooLow);1084		// Ensure user owns all pieces1085		let total_pieces = Self::total_pieces(collection.id, token).unwrap_or(u128::MAX);1086		let balance = <Balance<T>>::get((collection.id, token, owner));1087		ensure!(1088			total_pieces == balance,1089			<Error<T>>::RepartitionWhileNotOwningAllPieces1090		);10911092		<Balance<T>>::insert((collection.id, token, owner), amount);1093		<TotalSupply<T>>::insert((collection.id, token), amount);1094		Ok(())1095	}10961097	fn token_owner(collection_id: CollectionId, token_id: TokenId) -> Option<T::CrossAccountId> {1098		let mut owner = None;1099		let mut count = 0;1100		for key in Balance::<T>::iter_key_prefix((collection_id, token_id)) {1101			count += 1;1102			if count > 1 {1103				return None;1104			}1105			owner = Some(key);1106		}1107		owner1108	}11091110	fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option<u128> {1111		<TotalSupply<T>>::try_get((collection_id, token_id)).ok()1112	}11131114	pub fn set_collection_properties(1115		collection: &RefungibleHandle<T>,1116		sender: &T::CrossAccountId,1117		properties: Vec<Property>,1118	) -> DispatchResult {1119		<PalletCommon<T>>::set_collection_properties(collection, sender, properties)1120	}11211122	pub fn delete_collection_properties(1123		collection: &RefungibleHandle<T>,1124		sender: &T::CrossAccountId,1125		property_keys: Vec<PropertyKey>,1126	) -> DispatchResult {1127		<PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)1128	}11291130	pub fn set_token_property_permissions(1131		collection: &RefungibleHandle<T>,1132		sender: &T::CrossAccountId,1133		property_permissions: Vec<PropertyKeyPermission>,1134	) -> DispatchResult {1135		<PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)1136	}11371138	/// Returns 10 token in no particular order.1139	///1140	/// There is no direct way to get token holders in ascending order,1141	/// since `iter_prefix` returns values in no particular order.1142	/// Therefore, getting the 10 largest holders with a large value of holders1143	/// can lead to impact memory allocation + sorting with  `n * log (n)`.1144	pub fn token_owners(1145		collection_id: CollectionId,1146		token: TokenId,1147	) -> Option<Vec<T::CrossAccountId>> {1148		let res: Vec<T::CrossAccountId> = <Balance<T>>::iter_prefix((collection_id, token))1149			.map(|(owner, _amount)| owner)1150			.take(10)1151			.collect();11521153		if res.is_empty() {1154			None1155		} else {1156			Some(res)1157		}1158	}1159}
after · pallets/refungible/src/lib.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 Pallet18//!19//! The Refungible pallet provides functionality for handling refungible collections and tokens.20//!21//! - [`Config`]22//! - [`RefungibleHandle`]23//! - [`Pallet`]24//! - [`CommonWeights`]25//!26//! ## Overview27//!28//! The Refungible pallet provides functions for:29//!30//! - RFT collection creation and removal31//! - Minting and burning of RFT tokens32//! - Partition and repartition of RFT tokens33//! - Retrieving number of pieces of RFT token34//! - Retrieving account balances35//! - Transfering RFT token pieces36//! - Burning RFT token pieces37//! - Setting and checking allowance for RFT tokens38//!39//! ### Terminology40//!41//! - **RFT token:** Non fungible token that was partitioned to pieces. If an account owns all42//!   of the RFT token pieces than it owns the RFT token and can repartition it.43//!44//! - **RFT Collection:** A collection of RFT tokens. All RFT tokens are part of a collection.45//!   Each collection has its own settings and set of permissions.46//!47//! - **RFT token piece:** A fungible part of an RFT token.48//!49//! - **Balance:** RFT token pieces owned by an account50//!51//! - **Allowance:** Maximum number of RFT token pieces that one account is allowed to52//!   transfer from the balance of another account53//!54//! - **Burning:** The process of “deleting” a token from a collection or removing token pieces from55//!   an account balance.56//!57//! ### Implementations58//!59//! The Refungible pallet provides implementations for the following traits. If these traits provide60//! the functionality that you need, then you can avoid coupling with the Refungible pallet.61//!62//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight63//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing64//!   with collections65//! - [`RefungibleExtensions`](pallet_common::RefungibleExtensions): Functions specific for refungible66//!   collection67//!68//! ## Interface69//!70//! ### Dispatchable Functions71//!72//! - `init_collection` - Create RFT collection. RFT collection can be configured to allow or deny access for73//!   some accounts.74//! - `destroy_collection` - Destroy exising RFT collection. There should be no tokens in the collection.75//! - `burn` - Burn some amount of RFT token pieces owned by account. Burns the RFT token if no pieces left.76//! - `transfer` - Transfer some amount of RFT token pieces. Transfers should be enabled for RFT collection.77//!   Nests the RFT token if RFT token pieces are sent to another token.78//! - `create_item` - Mint RFT token in collection. Sender should have permission to mint tokens.79//! - `set_allowance` - Set allowance for another account to transfer balance from sender's account.80//! - `repartition` - Repartition token to selected number of pieces. Sender should own all existing pieces.81//!82//! ## Assumptions83//!84//! * Total number of pieces for one token shouldn't exceed `up_data_structs::MAX_REFUNGIBLE_PIECES`.85//! * Total number of tokens of all types shouldn't be greater than `up_data_structs::MAX_TOKEN_PREFIX_LENGTH`.86//! * Sender should be in collection's allow list to perform operations on tokens.8788#![cfg_attr(not(feature = "std"), no_std)]8990use crate::erc_token::ERC20Events;9192use codec::{Encode, Decode, MaxEncodedLen};93use core::ops::Deref;94use evm_coder::ToLog;95use frame_support::{BoundedVec, ensure, fail, storage::with_transaction, transactional};96use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};97use pallet_evm_coder_substrate::WithRecorder;98use pallet_common::{99	CommonCollectionOperations, Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,100};101use pallet_structure::Pallet as PalletStructure;102use scale_info::TypeInfo;103use sp_core::H160;104use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};105use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};106use up_data_structs::{107	AccessMode, budget::Budget, CollectionId, CreateCollectionData, CreateRefungibleExData,108	CustomDataLimit, mapping::TokenAddressMapping, MAX_REFUNGIBLE_PIECES, TokenId, Property,109	PropertyKey, PropertyKeyPermission, PropertyPermission, PropertyScope, PropertyValue,110	TrySetProperty,111};112113pub use pallet::*;114#[cfg(feature = "runtime-benchmarks")]115pub mod benchmarking;116pub mod common;117pub mod erc;118pub mod erc_token;119pub mod weights;120pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;121122#[struct_versioning::versioned(version = 2, upper)]123#[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]124pub struct ItemData {125	pub const_data: BoundedVec<u8, CustomDataLimit>,126127	#[version(..2)]128	pub variable_data: BoundedVec<u8, CustomDataLimit>,129}130131#[frame_support::pallet]132pub mod pallet {133	use super::*;134	use frame_support::{135		Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key,136		traits::StorageVersion,137	};138	use frame_system::pallet_prelude::*;139	use up_data_structs::{CollectionId, TokenId};140	use super::weights::WeightInfo;141142	#[pallet::error]143	pub enum Error<T> {144		/// Not Refungible item data used to mint in Refungible collection.145		NotRefungibleDataUsedToMintFungibleCollectionToken,146		/// Maximum refungibility exceeded147		WrongRefungiblePieces,148		/// Refungible token can't be repartitioned by user who isn't owns all pieces149		RepartitionWhileNotOwningAllPieces,150		/// Refungible token can't nest other tokens151		RefungibleDisallowsNesting,152		/// Setting item properties is not allowed153		SettingPropertiesNotAllowed,154	}155156	#[pallet::config]157	pub trait Config:158		frame_system::Config + pallet_common::Config + pallet_structure::Config159	{160		type WeightInfo: WeightInfo;161	}162163	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);164165	#[pallet::pallet]166	#[pallet::storage_version(STORAGE_VERSION)]167	#[pallet::generate_store(pub(super) trait Store)]168	pub struct Pallet<T>(_);169170	/// Amount of tokens minted for collection171	#[pallet::storage]172	pub type TokensMinted<T: Config> =173		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;174175	/// Amount of burnt tokens for collection176	#[pallet::storage]177	pub type TokensBurnt<T: Config> =178		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;179180	/// Custom data serialized to bytes for token181	#[pallet::storage]182	pub type TokenData<T: Config> = StorageNMap<183		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),184		Value = ItemData,185		QueryKind = ValueQuery,186	>;187188	#[pallet::storage]189	#[pallet::getter(fn token_properties)]190	pub type TokenProperties<T: Config> = StorageNMap<191		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),192		Value = up_data_structs::Properties,193		QueryKind = ValueQuery,194		OnEmpty = up_data_structs::TokenProperties,195	>;196197	/// Total amount of pieces for token198	#[pallet::storage]199	pub type TotalSupply<T: Config> = StorageNMap<200		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),201		Value = u128,202		QueryKind = ValueQuery,203	>;204205	/// Used to enumerate tokens owned by account206	#[pallet::storage]207	pub type Owned<T: Config> = StorageNMap<208		Key = (209			Key<Twox64Concat, CollectionId>,210			Key<Blake2_128Concat, T::CrossAccountId>,211			Key<Twox64Concat, TokenId>,212		),213		Value = bool,214		QueryKind = ValueQuery,215	>;216217	/// Amount of tokens owned by account218	#[pallet::storage]219	pub type AccountBalance<T: Config> = StorageNMap<220		Key = (221			Key<Twox64Concat, CollectionId>,222			// Owner223			Key<Blake2_128Concat, T::CrossAccountId>,224		),225		Value = u32,226		QueryKind = ValueQuery,227	>;228229	/// Amount of token pieces owned by account230	#[pallet::storage]231	pub type Balance<T: Config> = StorageNMap<232		Key = (233			Key<Twox64Concat, CollectionId>,234			Key<Twox64Concat, TokenId>,235			// Owner236			Key<Blake2_128Concat, T::CrossAccountId>,237		),238		Value = u128,239		QueryKind = ValueQuery,240	>;241242	/// Allowance set by an owner for a spender for a token243	#[pallet::storage]244	pub type Allowance<T: Config> = StorageNMap<245		Key = (246			Key<Twox64Concat, CollectionId>,247			Key<Twox64Concat, TokenId>,248			// Owner249			Key<Blake2_128, T::CrossAccountId>,250			// Spender251			Key<Blake2_128Concat, T::CrossAccountId>,252		),253		Value = u128,254		QueryKind = ValueQuery,255	>;256257	#[pallet::hooks]258	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {259		fn on_runtime_upgrade() -> Weight {260			if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {261				<TokenData<T>>::translate_values::<ItemDataVersion1, _>(|v| {262					Some(<ItemDataVersion2>::from(v))263				})264			}265266			0267		}268	}269}270271pub struct RefungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);272impl<T: Config> RefungibleHandle<T> {273	pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {274		Self(inner)275	}276	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {277		self.0278	}279	pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {280		&mut self.0281	}282}283284impl<T: Config> Deref for RefungibleHandle<T> {285	type Target = pallet_common::CollectionHandle<T>;286287	fn deref(&self) -> &Self::Target {288		&self.0289	}290}291292impl<T: Config> WithRecorder<T> for RefungibleHandle<T> {293	fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder<T> {294		self.0.recorder()295	}296	fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder<T> {297		self.0.into_recorder()298	}299}300301impl<T: Config> Pallet<T> {302	/// Get number of RFT tokens in collection303	pub fn total_supply(collection: &RefungibleHandle<T>) -> u32 {304		<TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)305	}306307	/// Check that RFT token exists308	///309	/// - `token`: Token ID.310	pub fn token_exists(collection: &RefungibleHandle<T>, token: TokenId) -> bool {311		<TotalSupply<T>>::contains_key((collection.id, token))312	}313314	pub fn set_scoped_token_property(315		collection_id: CollectionId,316		token_id: TokenId,317		scope: PropertyScope,318		property: Property,319	) -> DispatchResult {320		TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {321			properties.try_scoped_set(scope, property.key, property.value)322		})323		.map_err(<CommonError<T>>::from)?;324325		Ok(())326	}327328	pub fn set_scoped_token_properties(329		collection_id: CollectionId,330		token_id: TokenId,331		scope: PropertyScope,332		properties: impl Iterator<Item = Property>,333	) -> DispatchResult {334		TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {335			stored_properties.try_scoped_set_from_iter(scope, properties)336		})337		.map_err(<CommonError<T>>::from)?;338339		Ok(())340	}341}342343// unchecked calls skips any permission checks344impl<T: Config> Pallet<T> {345	/// Create RFT collection346	///347	/// `init_collection` will take non-refundable deposit for collection creation.348	///349	/// - `data`: Contains settings for collection limits and permissions.350	pub fn init_collection(351		owner: T::CrossAccountId,352		data: CreateCollectionData<T::AccountId>,353	) -> Result<CollectionId, DispatchError> {354		<PalletCommon<T>>::init_collection(owner, data, false)355	}356357	/// Destroy RFT collection358	///359	/// `destroy_collection` will throw error if collection contains any tokens.360	/// Only owner can destroy collection.361	pub fn destroy_collection(362		collection: RefungibleHandle<T>,363		sender: &T::CrossAccountId,364	) -> DispatchResult {365		let id = collection.id;366367		if Self::collection_has_tokens(id) {368			return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());369		}370371		// =========372373		PalletCommon::destroy_collection(collection.0, sender)?;374375		<TokensMinted<T>>::remove(id);376		<TokensBurnt<T>>::remove(id);377		<TokenData<T>>::remove_prefix((id,), None);378		<TotalSupply<T>>::remove_prefix((id,), None);379		<Balance<T>>::remove_prefix((id,), None);380		<Allowance<T>>::remove_prefix((id,), None);381		<Owned<T>>::remove_prefix((id,), None);382		<AccountBalance<T>>::remove_prefix((id,), None);383		Ok(())384	}385386	fn collection_has_tokens(collection_id: CollectionId) -> bool {387		<TokenData<T>>::iter_prefix((collection_id,))388			.next()389			.is_some()390	}391392	pub fn burn_token_unchecked(393		collection: &RefungibleHandle<T>,394		token_id: TokenId,395	) -> DispatchResult {396		let burnt = <TokensBurnt<T>>::get(collection.id)397			.checked_add(1)398			.ok_or(ArithmeticError::Overflow)?;399400		<TokensBurnt<T>>::insert(collection.id, burnt);401		<TokenData<T>>::remove((collection.id, token_id));402		<TokenProperties<T>>::remove((collection.id, token_id));403		<TotalSupply<T>>::remove((collection.id, token_id));404		<Balance<T>>::remove_prefix((collection.id, token_id), None);405		<Allowance<T>>::remove_prefix((collection.id, token_id), None);406		// TODO: ERC721 transfer event407		Ok(())408	}409410	/// Burn RFT token pieces411	///412	/// `burn` will decrease total amount of token pieces and amount owned by sender.413	/// `burn` can be called even if there are multiple owners of the RFT token.414	/// If sender wouldn't have any pieces left after `burn` than she will stop being415	/// one of the owners of the token. If there is no account that owns any pieces of416	/// the token than token will be burned too.417	///418	/// - `amount`: Amount of token pieces to burn.419	/// - `token`: Token who's pieces should be burned420	/// - `collection`: Collection that contains the token421	pub fn burn(422		collection: &RefungibleHandle<T>,423		owner: &T::CrossAccountId,424		token: TokenId,425		amount: u128,426	) -> DispatchResult {427		let total_supply = <TotalSupply<T>>::get((collection.id, token))428			.checked_sub(amount)429			.ok_or(<CommonError<T>>::TokenValueTooLow)?;430431		// This was probally last owner of this token?432		if total_supply == 0 {433			// Ensure user actually owns this amount434			ensure!(435				<Balance<T>>::get((collection.id, token, owner)) == amount,436				<CommonError<T>>::TokenValueTooLow437			);438			let account_balance = <AccountBalance<T>>::get((collection.id, owner))439				.checked_sub(1)440				// Should not occur441				.ok_or(ArithmeticError::Underflow)?;442443			// =========444445			<Owned<T>>::remove((collection.id, owner, token));446			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);447			<AccountBalance<T>>::insert((collection.id, owner), account_balance);448			Self::burn_token_unchecked(collection, token)?;449			<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(450				collection.id,451				token,452				owner.clone(),453				amount,454			));455			return Ok(());456		}457458		let balance = <Balance<T>>::get((collection.id, token, owner))459			.checked_sub(amount)460			.ok_or(<CommonError<T>>::TokenValueTooLow)?;461		let account_balance = if balance == 0 {462			<AccountBalance<T>>::get((collection.id, owner))463				.checked_sub(1)464				// Should not occur465				.ok_or(ArithmeticError::Underflow)?466		} else {467			0468		};469470		// =========471472		if balance == 0 {473			<Owned<T>>::remove((collection.id, owner, token));474			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);475			<Balance<T>>::remove((collection.id, token, owner));476			<AccountBalance<T>>::insert((collection.id, owner), account_balance);477		} else {478			<Balance<T>>::insert((collection.id, token, owner), balance);479		}480		<TotalSupply<T>>::insert((collection.id, token), total_supply);481482		<PalletEvm<T>>::deposit_log(483			ERC20Events::Transfer {484				from: *owner.as_eth(),485				to: H160::default(),486				value: amount.into(),487			}488			.to_log(T::EvmTokenAddressMapping::token_to_address(489				collection.id,490				token,491			)),492		);493		<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(494			collection.id,495			token,496			owner.clone(),497			amount,498		));499		Ok(())500	}501502	#[transactional]503	fn modify_token_properties(504		collection: &RefungibleHandle<T>,505		sender: &T::CrossAccountId,506		token_id: TokenId,507		properties: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,508		is_token_create: bool,509		nesting_budget: &dyn Budget,510	) -> DispatchResult {511		let is_collection_admin = || collection.is_owner_or_admin(sender);512		let is_token_owner = || -> Result<bool, DispatchError> {513			let balance = collection.balance(sender.clone(), token_id);514			let total_pieces: u128 =515				Self::total_pieces(collection.id, token_id).unwrap_or(u128::MAX);516			if balance != total_pieces {517				return Ok(false);518			}519520			let is_bundle_owner = <PalletStructure<T>>::check_indirectly_owned(521				sender.clone(),522				collection.id,523				token_id,524				None,525				nesting_budget,526			)?;527528			Ok(is_bundle_owner)529		};530531		for (key, value) in properties {532			let permission = <PalletCommon<T>>::property_permissions(collection.id)533				.get(&key)534				.cloned()535				.unwrap_or_else(PropertyPermission::none);536537			let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))538				.get(&key)539				.is_some();540541			match permission {542				PropertyPermission { mutable: false, .. } if is_property_exists => {543					return Err(<CommonError<T>>::NoPermission.into());544				}545546				PropertyPermission {547					collection_admin,548					token_owner,549					..550				} => {551					//TODO: investigate threats during public minting.552					let is_token_create =553						is_token_create && (collection_admin || token_owner) && value.is_some();554					if !(is_token_create555						|| (collection_admin && is_collection_admin())556						|| (token_owner && is_token_owner()?))557					{558						fail!(<CommonError<T>>::NoPermission);559					}560				}561			}562563			match value {564				Some(value) => {565					<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {566						properties.try_set(key.clone(), value)567					})568					.map_err(<CommonError<T>>::from)?;569570					<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(571						collection.id,572						token_id,573						key,574					));575				}576				None => {577					<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {578						properties.remove(&key)579					})580					.map_err(<CommonError<T>>::from)?;581582					<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(583						collection.id,584						token_id,585						key,586					));587				}588			}589		}590591		Ok(())592	}593594	pub fn set_token_properties(595		collection: &RefungibleHandle<T>,596		sender: &T::CrossAccountId,597		token_id: TokenId,598		properties: impl Iterator<Item = Property>,599		is_token_create: bool,600		nesting_budget: &dyn Budget,601	) -> DispatchResult {602		Self::modify_token_properties(603			collection,604			sender,605			token_id,606			properties.map(|p| (p.key, Some(p.value))),607			is_token_create,608			nesting_budget,609		)610	}611612	pub fn set_token_property(613		collection: &RefungibleHandle<T>,614		sender: &T::CrossAccountId,615		token_id: TokenId,616		property: Property,617		nesting_budget: &dyn Budget,618	) -> DispatchResult {619		let is_token_create = false;620621		Self::set_token_properties(622			collection,623			sender,624			token_id,625			[property].into_iter(),626			is_token_create,627			nesting_budget,628		)629	}630631	pub fn delete_token_properties(632		collection: &RefungibleHandle<T>,633		sender: &T::CrossAccountId,634		token_id: TokenId,635		property_keys: impl Iterator<Item = PropertyKey>,636		nesting_budget: &dyn Budget,637	) -> DispatchResult {638		let is_token_create = false;639640		Self::modify_token_properties(641			collection,642			sender,643			token_id,644			property_keys.into_iter().map(|key| (key, None)),645			is_token_create,646			nesting_budget,647		)648	}649650	pub fn delete_token_property(651		collection: &RefungibleHandle<T>,652		sender: &T::CrossAccountId,653		token_id: TokenId,654		property_key: PropertyKey,655		nesting_budget: &dyn Budget,656	) -> DispatchResult {657		Self::delete_token_properties(658			collection,659			sender,660			token_id,661			[property_key].into_iter(),662			nesting_budget,663		)664	}665666	/// Transfer RFT token pieces from one account to another.667	///668	/// If the sender is no longer owns any pieces after the `transfer` than she stops being an owner of the token.669	///670	/// - `from`: Owner of token pieces to transfer.671	/// - `to`: Recepient of transfered token pieces.672	/// - `amount`: Amount of token pieces to transfer.673	/// - `token`: Token whos pieces should be transfered674	/// - `collection`: Collection that contains the token675	pub fn transfer(676		collection: &RefungibleHandle<T>,677		from: &T::CrossAccountId,678		to: &T::CrossAccountId,679		token: TokenId,680		amount: u128,681		nesting_budget: &dyn Budget,682	) -> DispatchResult {683		ensure!(684			collection.limits.transfers_enabled(),685			<CommonError<T>>::TransferNotAllowed686		);687688		if collection.permissions.access() == AccessMode::AllowList {689			collection.check_allowlist(from)?;690			collection.check_allowlist(to)?;691		}692		<PalletCommon<T>>::ensure_correct_receiver(to)?;693694		let balance_from = <Balance<T>>::get((collection.id, token, from))695			.checked_sub(amount)696			.ok_or(<CommonError<T>>::TokenValueTooLow)?;697		let mut create_target = false;698		let from_to_differ = from != to;699		let balance_to = if from != to {700			let old_balance = <Balance<T>>::get((collection.id, token, to));701			if old_balance == 0 {702				create_target = true;703			}704			Some(705				old_balance706					.checked_add(amount)707					.ok_or(ArithmeticError::Overflow)?,708			)709		} else {710			None711		};712713		let account_balance_from = if balance_from == 0 {714			Some(715				<AccountBalance<T>>::get((collection.id, from))716					.checked_sub(1)717					// Should not occur718					.ok_or(ArithmeticError::Underflow)?,719			)720		} else {721			None722		};723		// Account data is created in token, AccountBalance should be increased724		// But only if from != to as we shouldn't check overflow in this case725		let account_balance_to = if create_target && from_to_differ {726			let account_balance_to = <AccountBalance<T>>::get((collection.id, to))727				.checked_add(1)728				.ok_or(ArithmeticError::Overflow)?;729			ensure!(730				account_balance_to < collection.limits.account_token_ownership_limit(),731				<CommonError<T>>::AccountTokenLimitExceeded,732			);733734			Some(account_balance_to)735		} else {736			None737		};738739		// =========740741		<PalletStructure<T>>::nest_if_sent_to_token(742			from.clone(),743			to,744			collection.id,745			token,746			nesting_budget,747		)?;748749		if let Some(balance_to) = balance_to {750			// from != to751			if balance_from == 0 {752				<Balance<T>>::remove((collection.id, token, from));753				<PalletStructure<T>>::unnest_if_nested(from, collection.id, token);754			} else {755				<Balance<T>>::insert((collection.id, token, from), balance_from);756			}757			<Balance<T>>::insert((collection.id, token, to), balance_to);758			if let Some(account_balance_from) = account_balance_from {759				<AccountBalance<T>>::insert((collection.id, from), account_balance_from);760				<Owned<T>>::remove((collection.id, from, token));761			}762			if let Some(account_balance_to) = account_balance_to {763				<AccountBalance<T>>::insert((collection.id, to), account_balance_to);764				<Owned<T>>::insert((collection.id, to, token), true);765			}766		}767768		<PalletEvm<T>>::deposit_log(769			ERC20Events::Transfer {770				from: *from.as_eth(),771				to: *to.as_eth(),772				value: amount.into(),773			}774			.to_log(T::EvmTokenAddressMapping::token_to_address(775				collection.id,776				token,777			)),778		);779		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(780			collection.id,781			token,782			from.clone(),783			to.clone(),784			amount,785		));786		Ok(())787	}788789	/// Batched operation to create multiple RFT tokens.790	///791	/// Same as `create_item` but creates multiple tokens.792	///793	/// - `data`: Same as 'data` in `create_item` but contains data for multiple tokens.794	pub fn create_multiple_items(795		collection: &RefungibleHandle<T>,796		sender: &T::CrossAccountId,797		data: Vec<CreateRefungibleExData<T::CrossAccountId>>,798		nesting_budget: &dyn Budget,799	) -> DispatchResult {800		if !collection.is_owner_or_admin(sender) {801			ensure!(802				collection.permissions.mint_mode(),803				<CommonError<T>>::PublicMintingNotAllowed804			);805			collection.check_allowlist(sender)?;806807			for item in data.iter() {808				for user in item.users.keys() {809					collection.check_allowlist(user)?;810				}811			}812		}813814		for item in data.iter() {815			for (owner, _) in item.users.iter() {816				<PalletCommon<T>>::ensure_correct_receiver(owner)?;817			}818		}819820		// Total pieces per tokens821		let totals = data822			.iter()823			.map(|data| {824				Ok(data825					.users826					.iter()827					.map(|u| u.1)828					.try_fold(0u128, |acc, v| acc.checked_add(*v))829					.ok_or(ArithmeticError::Overflow)?)830			})831			.collect::<Result<Vec<_>, DispatchError>>()?;832		for total in &totals {833			ensure!(834				*total <= MAX_REFUNGIBLE_PIECES,835				<Error<T>>::WrongRefungiblePieces836			);837		}838839		let first_token_id = <TokensMinted<T>>::get(collection.id);840		let tokens_minted = first_token_id841			.checked_add(data.len() as u32)842			.ok_or(ArithmeticError::Overflow)?;843		ensure!(844			tokens_minted < collection.limits.token_limit(),845			<CommonError<T>>::CollectionTokenLimitExceeded846		);847848		let mut balances = BTreeMap::new();849		for data in &data {850			for owner in data.users.keys() {851				let balance = balances852					.entry(owner)853					.or_insert_with(|| <AccountBalance<T>>::get((collection.id, owner)));854				*balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;855856				ensure!(857					*balance <= collection.limits.account_token_ownership_limit(),858					<CommonError<T>>::AccountTokenLimitExceeded,859				);860			}861		}862863		for (i, token) in data.iter().enumerate() {864			let token_id = TokenId(first_token_id + i as u32 + 1);865			for (to, _) in token.users.iter() {866				<PalletStructure<T>>::check_nesting(867					sender.clone(),868					to,869					collection.id,870					token_id,871					nesting_budget,872				)?;873			}874		}875876		// =========877878		with_transaction(|| {879			for (i, data) in data.iter().enumerate() {880				let token_id = first_token_id + i as u32 + 1;881				<TotalSupply<T>>::insert((collection.id, token_id), totals[i]);882883				<TokenData<T>>::insert(884					(collection.id, token_id),885					ItemData {886						const_data: data.const_data.clone(),887					},888				);889890				for (user, amount) in data.users.iter() {891					if *amount == 0 {892						continue;893					}894					<Balance<T>>::insert((collection.id, token_id, &user), amount);895					<Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);896					<PalletStructure<T>>::nest_if_sent_to_token_unchecked(897						user,898						collection.id,899						TokenId(token_id),900					);901				}902903				if let Err(e) = Self::set_token_properties(904					collection,905					sender,906					TokenId(token_id),907					data.properties.clone().into_iter(),908					true,909					nesting_budget,910				) {911					return TransactionOutcome::Rollback(Err(e));912				}913			}914			TransactionOutcome::Commit(Ok(()))915		})?;916917		<TokensMinted<T>>::insert(collection.id, tokens_minted);918919		for (account, balance) in balances {920			<AccountBalance<T>>::insert((collection.id, account), balance);921		}922923		for (i, token) in data.into_iter().enumerate() {924			let token_id = first_token_id + i as u32 + 1;925926			for (user, amount) in token.users.into_iter() {927				if amount == 0 {928					continue;929				}930931				<PalletEvm<T>>::deposit_log(932					ERC20Events::Transfer {933						from: H160::default(),934						to: *user.as_eth(),935						value: amount.into(),936					}937					.to_log(T::EvmTokenAddressMapping::token_to_address(938						collection.id,939						TokenId(token_id),940					)),941				);942				<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(943					collection.id,944					TokenId(token_id),945					user,946					amount,947				));948			}949		}950		Ok(())951	}952953	pub fn set_allowance_unchecked(954		collection: &RefungibleHandle<T>,955		sender: &T::CrossAccountId,956		spender: &T::CrossAccountId,957		token: TokenId,958		amount: u128,959	) {960		if amount == 0 {961			<Allowance<T>>::remove((collection.id, token, sender, spender));962		} else {963			<Allowance<T>>::insert((collection.id, token, sender, spender), amount);964		}965966		<PalletEvm<T>>::deposit_log(967			ERC20Events::Approval {968				owner: *sender.as_eth(),969				spender: *spender.as_eth(),970				value: amount.into(),971			}972			.to_log(T::EvmTokenAddressMapping::token_to_address(973				collection.id,974				token,975			)),976		);977		<PalletCommon<T>>::deposit_event(CommonEvent::Approved(978			collection.id,979			token,980			sender.clone(),981			spender.clone(),982			amount,983		))984	}985986	/// Set allowance for the spender to `transfer` or `burn` sender's token pieces.987	///988	/// - `amount`: Amount of token pieces the spender is allowed to `transfer` or `burn.989	pub fn set_allowance(990		collection: &RefungibleHandle<T>,991		sender: &T::CrossAccountId,992		spender: &T::CrossAccountId,993		token: TokenId,994		amount: u128,995	) -> DispatchResult {996		if collection.permissions.access() == AccessMode::AllowList {997			collection.check_allowlist(sender)?;998			collection.check_allowlist(spender)?;999		}10001001		<PalletCommon<T>>::ensure_correct_receiver(spender)?;10021003		if <Balance<T>>::get((collection.id, token, sender)) < amount {1004			ensure!(1005				collection.ignores_owned_amount(sender) && Self::token_exists(collection, token),1006				<CommonError<T>>::CantApproveMoreThanOwned1007			);1008		}10091010		// =========10111012		Self::set_allowance_unchecked(collection, sender, spender, token, amount);1013		Ok(())1014	}10151016	/// Returns allowance, which should be set after transaction1017	fn check_allowed(1018		collection: &RefungibleHandle<T>,1019		spender: &T::CrossAccountId,1020		from: &T::CrossAccountId,1021		token: TokenId,1022		amount: u128,1023		nesting_budget: &dyn Budget,1024	) -> Result<Option<u128>, DispatchError> {1025		if spender.conv_eq(from) {1026			return Ok(None);1027		}1028		if collection.permissions.access() == AccessMode::AllowList {1029			// `from`, `to` checked in [`transfer`]1030			collection.check_allowlist(spender)?;1031		}1032		if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1033			// TODO: should collection owner be allowed to perform this transfer?1034			ensure!(1035				<PalletStructure<T>>::check_indirectly_owned(1036					spender.clone(),1037					source.0,1038					source.1,1039					None,1040					nesting_budget1041				)?,1042				<CommonError<T>>::ApprovedValueTooLow,1043			);1044			return Ok(None);1045		}1046		let allowance =1047			<Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);1048		if allowance.is_none() {1049			ensure!(1050				collection.ignores_allowance(spender),1051				<CommonError<T>>::ApprovedValueTooLow1052			);1053		}1054		Ok(allowance)1055	}10561057	/// Transfer RFT token pieces from one account to another.1058	///1059	/// Same as the [`transfer`] but spender doesn't needs to be an owner of the token pieces.1060	/// The owner should set allowance for the spender to transfer pieces.1061	///1062	/// [`transfer`]: struct.Pallet.html#method.transfer1063	pub fn transfer_from(1064		collection: &RefungibleHandle<T>,1065		spender: &T::CrossAccountId,1066		from: &T::CrossAccountId,1067		to: &T::CrossAccountId,1068		token: TokenId,1069		amount: u128,1070		nesting_budget: &dyn Budget,1071	) -> DispatchResult {1072		let allowance =1073			Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;10741075		// =========10761077		Self::transfer(collection, from, to, token, amount, nesting_budget)?;1078		if let Some(allowance) = allowance {1079			Self::set_allowance_unchecked(collection, from, spender, token, allowance);1080		}1081		Ok(())1082	}10831084	/// Burn RFT token pieces from the account.1085	///1086	/// Same as the [`burn`] but spender doesn't need to be an owner of the token pieces. The owner should1087	/// set allowance for the spender to burn pieces1088	///1089	/// [`burn`]: struct.Pallet.html#method.burn1090	pub fn burn_from(1091		collection: &RefungibleHandle<T>,1092		spender: &T::CrossAccountId,1093		from: &T::CrossAccountId,1094		token: TokenId,1095		amount: u128,1096		nesting_budget: &dyn Budget,1097	) -> DispatchResult {1098		let allowance =1099			Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;11001101		// =========11021103		Self::burn(collection, from, token, amount)?;1104		if let Some(allowance) = allowance {1105			Self::set_allowance_unchecked(collection, from, spender, token, allowance);1106		}1107		Ok(())1108	}11091110	/// Create RFT token.1111	///1112	/// The sender should be the owner/admin of the collection or collection should be configured1113	/// to allow public minting.1114	///1115	/// - `data`: Contains list of users who will become the owners of the token pieces and amount1116	///   of token pieces they will receive.1117	pub fn create_item(1118		collection: &RefungibleHandle<T>,1119		sender: &T::CrossAccountId,1120		data: CreateRefungibleExData<T::CrossAccountId>,1121		nesting_budget: &dyn Budget,1122	) -> DispatchResult {1123		Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1124	}11251126	/// Repartition RFT token.1127	///1128	/// `repartition` will set token balance of the sender and total amount of token pieces.1129	/// Sender should own all of the token pieces. `repartition' could be done even if some1130	/// token pieces were burned before.1131	///1132	/// - `amount`: Total amount of token pieces that the token will have after `repartition`.1133	pub fn repartition(1134		collection: &RefungibleHandle<T>,1135		owner: &T::CrossAccountId,1136		token: TokenId,1137		amount: u128,1138	) -> DispatchResult {1139		ensure!(1140			amount <= MAX_REFUNGIBLE_PIECES,1141			<Error<T>>::WrongRefungiblePieces1142		);1143		ensure!(amount > 0, <CommonError<T>>::TokenValueTooLow);1144		// Ensure user owns all pieces1145		let total_pieces = Self::total_pieces(collection.id, token).unwrap_or(u128::MAX);1146		let balance = <Balance<T>>::get((collection.id, token, owner));1147		ensure!(1148			total_pieces == balance,1149			<Error<T>>::RepartitionWhileNotOwningAllPieces1150		);11511152		<Balance<T>>::insert((collection.id, token, owner), amount);1153		<TotalSupply<T>>::insert((collection.id, token), amount);11541155		if amount > total_pieces {1156			let mint_amount = amount - total_pieces;1157			<PalletEvm<T>>::deposit_log(1158				ERC20Events::Transfer {1159					from: H160::default(),1160					to: *owner.as_eth(),1161					value: mint_amount.into(),1162				}1163				.to_log(T::EvmTokenAddressMapping::token_to_address(1164					collection.id,1165					token,1166				)),1167			);1168			<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(1169				collection.id,1170				token,1171				owner.clone(),1172				mint_amount,1173			));1174		} else if total_pieces > amount {1175			let burn_amount = total_pieces - amount;1176			<PalletEvm<T>>::deposit_log(1177				ERC20Events::Transfer {1178					from: *owner.as_eth(),1179					to: H160::default(),1180					value: burn_amount.into(),1181				}1182				.to_log(T::EvmTokenAddressMapping::token_to_address(1183					collection.id,1184					token,1185				)),1186			);1187			<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(1188				collection.id,1189				token,1190				owner.clone(),1191				burn_amount,1192			));1193		}11941195		Ok(())1196	}11971198	fn token_owner(collection_id: CollectionId, token_id: TokenId) -> Option<T::CrossAccountId> {1199		let mut owner = None;1200		let mut count = 0;1201		for key in Balance::<T>::iter_key_prefix((collection_id, token_id)) {1202			count += 1;1203			if count > 1 {1204				return None;1205			}1206			owner = Some(key);1207		}1208		owner1209	}12101211	fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option<u128> {1212		<TotalSupply<T>>::try_get((collection_id, token_id)).ok()1213	}12141215	pub fn set_collection_properties(1216		collection: &RefungibleHandle<T>,1217		sender: &T::CrossAccountId,1218		properties: Vec<Property>,1219	) -> DispatchResult {1220		<PalletCommon<T>>::set_collection_properties(collection, sender, properties)1221	}12221223	pub fn delete_collection_properties(1224		collection: &RefungibleHandle<T>,1225		sender: &T::CrossAccountId,1226		property_keys: Vec<PropertyKey>,1227	) -> DispatchResult {1228		<PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)1229	}12301231	pub fn set_token_property_permissions(1232		collection: &RefungibleHandle<T>,1233		sender: &T::CrossAccountId,1234		property_permissions: Vec<PropertyKeyPermission>,1235	) -> DispatchResult {1236		<PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)1237	}12381239	/// Returns 10 token in no particular order.1240	///1241	/// There is no direct way to get token holders in ascending order,1242	/// since `iter_prefix` returns values in no particular order.1243	/// Therefore, getting the 10 largest holders with a large value of holders1244	/// can lead to impact memory allocation + sorting with  `n * log (n)`.1245	pub fn token_owners(1246		collection_id: CollectionId,1247		token: TokenId,1248	) -> Option<Vec<T::CrossAccountId>> {1249		let res: Vec<T::CrossAccountId> = <Balance<T>>::iter_prefix((collection_id, token))1250			.map(|(owner, _amount)| owner)1251			.take(10)1252			.collect();12531254		if res.is_empty() {1255			None1256		} else {1257			Some(res)1258		}1259	}1260}
modifiedpallets/refungible/src/stubs/UniqueRefungibleToken.rawdiffbeforeafterboth

binary blob — no preview

addedpallets/refungible/src/stubs/UniqueRefungibleToken.soldiffbeforeafterboth
--- /dev/null
+++ b/pallets/refungible/src/stubs/UniqueRefungibleToken.sol
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: OTHER
+// This code is automatically generated
+
+pragma solidity >=0.8.0 <0.9.0;
+
+// Common stubs holder
+contract Dummy {
+	uint8 dummy;
+	string stub_error = "this contract is implemented in native";
+}
+
+contract ERC165 is Dummy {
+	function supportsInterface(bytes4 interfaceID)
+		external
+		view
+		returns (bool)
+	{
+		require(false, stub_error);
+		interfaceID;
+		return true;
+	}
+}
+
+// Inline
+contract ERC20Events {
+	event Transfer(address indexed from, address indexed to, uint256 value);
+	event Approval(
+		address indexed owner,
+		address indexed spender,
+		uint256 value
+	);
+}
+
+// Selector: 942e8b22
+contract ERC20 is Dummy, ERC165, ERC20Events {
+	// Selector: name() 06fdde03
+	function name() public view returns (string memory) {
+		require(false, stub_error);
+		dummy;
+		return "";
+	}
+
+	// Selector: symbol() 95d89b41
+	function symbol() public view returns (string memory) {
+		require(false, stub_error);
+		dummy;
+		return "";
+	}
+
+	// Selector: totalSupply() 18160ddd
+	function totalSupply() public view returns (uint256) {
+		require(false, stub_error);
+		dummy;
+		return 0;
+	}
+
+	// Selector: decimals() 313ce567
+	function decimals() public view returns (uint8) {
+		require(false, stub_error);
+		dummy;
+		return 0;
+	}
+
+	// Selector: balanceOf(address) 70a08231
+	function balanceOf(address owner) public view returns (uint256) {
+		require(false, stub_error);
+		owner;
+		dummy;
+		return 0;
+	}
+
+	// Selector: transfer(address,uint256) a9059cbb
+	function transfer(address to, uint256 amount) public returns (bool) {
+		require(false, stub_error);
+		to;
+		amount;
+		dummy = 0;
+		return false;
+	}
+
+	// Selector: transferFrom(address,address,uint256) 23b872dd
+	function transferFrom(
+		address from,
+		address to,
+		uint256 amount
+	) public returns (bool) {
+		require(false, stub_error);
+		from;
+		to;
+		amount;
+		dummy = 0;
+		return false;
+	}
+
+	// Selector: approve(address,uint256) 095ea7b3
+	function approve(address spender, uint256 amount) public returns (bool) {
+		require(false, stub_error);
+		spender;
+		amount;
+		dummy = 0;
+		return false;
+	}
+
+	// Selector: allowance(address,address) dd62ed3e
+	function allowance(address owner, address spender)
+		public
+		view
+		returns (uint256)
+	{
+		require(false, stub_error);
+		owner;
+		spender;
+		dummy;
+		return 0;
+	}
+}
+
+// Selector: ab8deb37
+contract ERC20UniqueExtensions is Dummy, ERC165 {
+	// Selector: burnFrom(address,uint256) 79cc6790
+	function burnFrom(address from, uint256 amount) public returns (bool) {
+		require(false, stub_error);
+		from;
+		amount;
+		dummy = 0;
+		return false;
+	}
+
+	// Selector: repartition(uint256) d2418ca7
+	function repartition(uint256 amount) public returns (bool) {
+		require(false, stub_error);
+		amount;
+		dummy = 0;
+		return false;
+	}
+}
+
+contract UniqueRefungibleToken is Dummy, ERC165, ERC20, ERC20UniqueExtensions {}
modifiedruntime/common/src/dispatch.rsdiffbeforeafterboth
--- a/runtime/common/src/dispatch.rs
+++ b/runtime/common/src/dispatch.rs
@@ -25,7 +25,9 @@
 pub use pallet_common::dispatch::CollectionDispatch;
 use pallet_fungible::{Pallet as PalletFungible, FungibleHandle};
 use pallet_nonfungible::{Pallet as PalletNonfungible, NonfungibleHandle};
-use pallet_refungible::{Pallet as PalletRefungible, RefungibleHandle, erc::RefungibleTokenHandle};
+use pallet_refungible::{
+	Pallet as PalletRefungible, RefungibleHandle, erc_token::RefungibleTokenHandle,
+};
 use up_data_structs::{
 	CollectionMode, CreateCollectionData, MAX_DECIMAL_POINTS, mapping::TokenAddressMapping,
 };
addedtests/src/eth/api/UniqueRefungibleToken.soldiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/api/UniqueRefungibleToken.sol
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: OTHER
+// This code is automatically generated
+
+pragma solidity >=0.8.0 <0.9.0;
+
+// Common stubs holder
+interface Dummy {
+
+}
+
+interface ERC165 is Dummy {
+	function supportsInterface(bytes4 interfaceID) external view returns (bool);
+}
+
+// Inline
+interface ERC20Events {
+	event Transfer(address indexed from, address indexed to, uint256 value);
+	event Approval(
+		address indexed owner,
+		address indexed spender,
+		uint256 value
+	);
+}
+
+// Selector: 942e8b22
+interface ERC20 is Dummy, ERC165, ERC20Events {
+	// Selector: name() 06fdde03
+	function name() external view returns (string memory);
+
+	// Selector: symbol() 95d89b41
+	function symbol() external view returns (string memory);
+
+	// Selector: totalSupply() 18160ddd
+	function totalSupply() external view returns (uint256);
+
+	// Selector: decimals() 313ce567
+	function decimals() external view returns (uint8);
+
+	// Selector: balanceOf(address) 70a08231
+	function balanceOf(address owner) external view returns (uint256);
+
+	// Selector: transfer(address,uint256) a9059cbb
+	function transfer(address to, uint256 amount) external returns (bool);
+
+	// Selector: transferFrom(address,address,uint256) 23b872dd
+	function transferFrom(
+		address from,
+		address to,
+		uint256 amount
+	) external returns (bool);
+
+	// Selector: approve(address,uint256) 095ea7b3
+	function approve(address spender, uint256 amount) external returns (bool);
+
+	// Selector: allowance(address,address) dd62ed3e
+	function allowance(address owner, address spender)
+		external
+		view
+		returns (uint256);
+}
+
+// Selector: ab8deb37
+interface ERC20UniqueExtensions is Dummy, ERC165 {
+	// Selector: burnFrom(address,uint256) 79cc6790
+	function burnFrom(address from, uint256 amount) external returns (bool);
+
+	// Selector: repartition(uint256) d2418ca7
+	function repartition(uint256 amount) external returns (bool);
+}
+
+interface UniqueRefungibleToken is
+	Dummy,
+	ERC165,
+	ERC20,
+	ERC20UniqueExtensions
+{}
addedtests/src/eth/reFungibleToken.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/reFungibleToken.test.ts
@@ -0,0 +1,458 @@
+// 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 {approve, createCollection, createRefungibleToken, transfer, transferFrom, UNIQUE} from '../util/helpers';
+import {createEthAccount, createEthAccountWithBalance, GAS_ARGS, itWeb3, normalizeEvents, recordEthFee, recordEvents, subToEth, tokenIdToAddress, transferBalanceToEth} from './util/helpers';
+import reFungibleTokenAbi from './reFungibleTokenAbi.json';
+
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+chai.use(chaiAsPromised);
+const expect = chai.expect;
+
+describe('Refungible token: Information getting', () => {
+  itWeb3('totalSupply', async ({api, web3, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: caller})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: caller, ...GAS_ARGS});
+    const totalSupply = await contract.methods.totalSupply().call();
+
+    expect(totalSupply).to.equal('200');
+  });
+
+  itWeb3('balanceOf', async ({api, web3, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: caller})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: caller, ...GAS_ARGS});
+    const balance = await contract.methods.balanceOf(caller).call();
+
+    expect(balance).to.equal('200');
+  });
+
+  itWeb3('decimals', async ({api, web3, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: caller})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: caller, ...GAS_ARGS});
+    const decimals = await contract.methods.decimals().call();
+
+    expect(decimals).to.equal('0');
+  });
+});
+
+describe('Refungible: Plain calls', () => {
+  itWeb3('Can perform approve()', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+
+    const spender = createEthAccount(web3);
+
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    {
+      const result = await contract.methods.approve(spender, 100).send({from: owner});
+      const events = normalizeEvents(result.events);
+
+      expect(events).to.be.deep.equal([
+        {
+          address,
+          event: 'Approval',
+          args: {
+            owner,
+            spender,
+            value: '100',
+          },
+        },
+      ]);
+    }
+
+    {
+      const allowance = await contract.methods.allowance(owner, spender).call();
+      expect(+allowance).to.equal(100);
+    }
+  });
+
+  itWeb3('Can perform transferFrom()', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = createEthAccount(web3);
+    await transferBalanceToEth(api, alice, owner);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+    const spender = createEthAccount(web3);
+    await transferBalanceToEth(api, alice, spender);
+
+    const receiver = createEthAccount(web3);
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    await contract.methods.approve(spender, 100).send();
+
+    {
+      const result = await contract.methods.transferFrom(owner, receiver, 49).send({from: spender});
+      const events = normalizeEvents(result.events);
+      expect(events).to.be.deep.equal([
+        {
+          address,
+          event: 'Transfer',
+          args: {
+            from: owner,
+            to: receiver,
+            value: '49',
+          },
+        },
+        {
+          address,
+          event: 'Approval',
+          args: {
+            owner,
+            spender,
+            value: '51',
+          },
+        },
+      ]);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(receiver).call();
+      expect(+balance).to.equal(49);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(owner).call();
+      expect(+balance).to.equal(151);
+    }
+  });
+
+  itWeb3('Can perform transfer()', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = createEthAccount(web3);
+    await transferBalanceToEth(api, alice, owner);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+    const receiver = createEthAccount(web3);
+    await transferBalanceToEth(api, alice, receiver);
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    {
+      const result = await contract.methods.transfer(receiver, 50).send({from: owner});
+      const events = normalizeEvents(result.events);
+      expect(events).to.be.deep.equal([
+        {
+          address,
+          event: 'Transfer',
+          args: {
+            from: owner,
+            to: receiver,
+            value: '50',
+          },
+        },
+      ]);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(owner).call();
+      expect(+balance).to.equal(150);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(receiver).call();
+      expect(+balance).to.equal(50);
+    }
+  });
+
+  itWeb3('Can perform repartition()', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = createEthAccount(web3);
+    await transferBalanceToEth(api, alice, owner);
+
+    const receiver = createEthAccount(web3);
+    await transferBalanceToEth(api, alice, receiver);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n, {Ethereum: owner})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    await contract.methods.repartition(200).send({from: owner});
+    expect(+await contract.methods.balanceOf(owner).call()).to.be.equal(200);
+    await contract.methods.transfer(receiver, 110).send({from: owner});
+    expect(+await contract.methods.balanceOf(owner).call()).to.be.equal(90);
+    expect(+await contract.methods.balanceOf(receiver).call()).to.be.equal(110);
+    
+    await expect(contract.methods.repartition(80).send({from: owner})).to.eventually.be.rejected;
+
+    await contract.methods.transfer(receiver, 90).send({from: owner});
+    expect(+await contract.methods.balanceOf(owner).call()).to.be.equal(0);
+    expect(+await contract.methods.balanceOf(receiver).call()).to.be.equal(200);
+
+    await contract.methods.repartition(150).send({from: receiver});
+    await expect(contract.methods.transfer(owner, 160).send({from: receiver})).to.eventually.be.rejected;
+    expect(+await contract.methods.balanceOf(receiver).call()).to.be.equal(150);
+  });
+
+  itWeb3('Can repartition with increased amount', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = createEthAccount(web3);
+    await transferBalanceToEth(api, alice, owner);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n, {Ethereum: owner})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    const result = await contract.methods.repartition(200).send();
+    const events = normalizeEvents(result.events);
+
+    expect(events).to.deep.equal([
+      {
+        address,
+        event: 'Transfer',
+        args: {
+          from: '0x0000000000000000000000000000000000000000',
+          to: owner,
+          value: '100',
+        },
+      },
+    ]);
+  });
+
+  itWeb3('Can repartition with decreased amount', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = createEthAccount(web3);
+    await transferBalanceToEth(api, alice, owner);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n, {Ethereum: owner})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    const result = await contract.methods.repartition(50).send();
+    const events = normalizeEvents(result.events);
+    expect(events).to.deep.equal([
+      {
+        address,
+        event: 'Transfer',
+        args: {
+          from: owner,
+          to: '0x0000000000000000000000000000000000000000',
+          value: '50',
+        },
+      },
+    ]);
+  });
+});
+
+describe('Refungible: Fees', () => {
+  itWeb3('approve() call fee is less than 0.2UNQ', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const spender = createEthAccount(web3);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    const cost = await recordEthFee(api, owner, () => contract.methods.approve(spender, 100).send({from: owner}));
+    expect(cost < BigInt(0.2 * Number(UNIQUE)));
+  });
+
+  itWeb3('transferFrom() call fee is less than 0.2UNQ', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const spender = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    await contract.methods.approve(spender, 100).send({from: owner});
+
+    const cost = await recordEthFee(api, spender, () => contract.methods.transferFrom(owner, spender, 100).send({from: spender}));
+    expect(cost < BigInt(0.2 * Number(UNIQUE)));
+  });
+
+  itWeb3('transfer() call fee is less than 0.2UNQ', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const receiver = createEthAccount(web3);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    const cost = await recordEthFee(api, owner, () => contract.methods.transfer(receiver, 100).send({from: owner}));
+    expect(cost < BigInt(0.2 * Number(UNIQUE)));
+  });
+});
+
+describe('Refungible: Substrate calls', () => {
+  itWeb3('Events emitted for approve()', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+
+    const receiver = createEthAccount(web3);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n)).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address);
+
+    const events = await recordEvents(contract, async () => {
+      expect(await approve(api, collectionId, tokenId, alice, {Ethereum: receiver}, 100n)).to.be.true;
+    });
+
+    expect(events).to.be.deep.equal([
+      {
+        address,
+        event: 'Approval',
+        args: {
+          owner: subToEth(alice.address),
+          spender: receiver,
+          value: '100',
+        },
+      },
+    ]);
+  });
+
+  itWeb3('Events emitted for transferFrom()', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+    const bob = privateKeyWrapper('//Bob');
+
+    const receiver = createEthAccount(web3);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n)).itemId;
+    expect(await approve(api, collectionId, tokenId, alice, bob.address, 100n)).to.be.true;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address);
+
+    const events = await recordEvents(contract, async () => {
+      expect(await transferFrom(api, collectionId, tokenId, bob, alice, {Ethereum: receiver},  51n)).to.be.true;
+    });
+
+    expect(events).to.be.deep.equal([
+      {
+        address,
+        event: 'Transfer',
+        args: {
+          from: subToEth(alice.address),
+          to: receiver,
+          value: '51',
+        },
+      },
+      {
+        address,
+        event: 'Approval',
+        args: {
+          owner: subToEth(alice.address),
+          spender: subToEth(bob.address),
+          value: '49',
+        },
+      },
+    ]);
+  });
+
+  itWeb3('Events emitted for transfer()', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+
+    const receiver = createEthAccount(web3);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n)).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address);
+
+    const events = await recordEvents(contract, async () => {
+      expect(await transfer(api, collectionId, tokenId, alice, {Ethereum: receiver},  51n)).to.be.true;
+    });
+
+    expect(events).to.be.deep.equal([
+      {
+        address,
+        event: 'Transfer',
+        args: {
+          from: subToEth(alice.address),
+          to: receiver,
+          value: '51',
+        },
+      },
+    ]);
+  });
+});
addedtests/src/eth/reFungibleTokenAbi.jsondiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/reFungibleTokenAbi.json
@@ -0,0 +1,158 @@
+[
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "owner",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "spender",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint256",
+        "name": "value",
+        "type": "uint256"
+      }
+    ],
+    "name": "Approval",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "from",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "to",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint256",
+        "name": "value",
+        "type": "uint256"
+      }
+    ],
+    "name": "Transfer",
+    "type": "event"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "owner", "type": "address" },
+      { "internalType": "address", "name": "spender", "type": "address" }
+    ],
+    "name": "allowance",
+    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "spender", "type": "address" },
+      { "internalType": "uint256", "name": "amount", "type": "uint256" }
+    ],
+    "name": "approve",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "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": "address", "name": "from", "type": "address" },
+      { "internalType": "uint256", "name": "amount", "type": "uint256" }
+    ],
+    "name": "burnFrom",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "decimals",
+    "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "name",
+    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "amount", "type": "uint256" }
+    ],
+    "name": "repartition",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "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": [],
+    "name": "totalSupply",
+    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256", "name": "amount", "type": "uint256" }
+    ],
+    "name": "transfer",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "from", "type": "address" },
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256", "name": "amount", "type": "uint256" }
+    ],
+    "name": "transferFrom",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  }
+]
modifiedtests/src/refungible.test.tsdiffbeforeafterboth
--- a/tests/src/refungible.test.ts
+++ b/tests/src/refungible.test.ts
@@ -14,7 +14,7 @@
 // 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 {default as usingApi, executeTransaction} from './substrate/substrate-api';
+import {default as usingApi, submitTransactionAsync} from './substrate/substrate-api';
 import {IKeyringPair} from '@polkadot/types/types';
 import {
   createCollectionExpectSuccess,
@@ -34,6 +34,8 @@
   getDetailedCollectionInfo,
   normalizeAccountId,
   CrossAccountId,
+  getCreateItemsResult,
+  getDestroyItemsResult,
 } from './util/helpers';
 
 import chai from 'chai';
@@ -226,6 +228,46 @@
       await expect(transfer(api, collectionId, tokenId, bob, alice, 160n)).to.eventually.be.rejected;
     });
   });
+
+  it('Repartition with increased amount', async () => {
+    await usingApi(async api => {
+      const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+      const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n)).itemId;
+
+      const tx = api.tx.unique.repartition(collectionId, tokenId, 200n);
+      const events = await submitTransactionAsync(alice, tx);
+      const substrateEvents = getCreateItemsResult(events);
+      expect(substrateEvents).to.include.deep.members([
+        {
+          success: true,
+          collectionId,
+          itemId: tokenId,
+          recipient: {Substrate: alice.address},
+          amount: 100,
+        },
+      ]);
+    });
+  });
+
+  it('Repartition with decreased amount', async () => {
+    await usingApi(async api => {
+      const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+      const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n)).itemId;
+
+      const tx = api.tx.unique.repartition(collectionId, tokenId, 50n);
+      const events = await submitTransactionAsync(alice, tx);
+      const substrateEvents = getDestroyItemsResult(events);
+      expect(substrateEvents).to.include.deep.members([
+        {
+          success: true,
+          collectionId,
+          itemId: tokenId,
+          owner: {Substrate: alice.address},
+          amount: 50,
+        },
+      ]);
+    });
+  });
 });
 
 describe('Test Refungible properties:', () => {
modifiedtests/src/util/helpers.tsdiffbeforeafterboth
--- a/tests/src/util/helpers.ts
+++ b/tests/src/util/helpers.ts
@@ -103,6 +103,15 @@
   collectionId: number;
   itemId: number;
   recipient?: CrossAccountId;
+  amount?: number;
+}
+
+interface DestroyItemResult {
+  success: boolean;
+  collectionId: number;
+  itemId: number;
+  owner: CrossAccountId;
+  amount: number;
 }
 
 interface TransferResult {
@@ -220,12 +229,14 @@
     const collectionId = parseInt(data[0].toString(), 10);
     const itemId = parseInt(data[1].toString(), 10);
     const recipient = normalizeAccountId(data[2].toJSON() as any);
+    const amount = parseInt(data[3].toString(), 10);
 
     const itemRes: CreateItemResult = {
       success: true,
       collectionId,
       itemId,
       recipient,
+      amount,
     };
 
     results.push(itemRes);
@@ -255,6 +266,31 @@
   return result;
 }
 
+export function getDestroyItemsResult(events: EventRecord[]): DestroyItemResult[] {
+  const results: DestroyItemResult[] = [];
+  
+  const genericResult = getGenericResult<DestroyItemResult[]>(events, 'common', 'ItemDestroyed', (data) => {
+    const collectionId = parseInt(data[0].toString(), 10);
+    const itemId = parseInt(data[1].toString(), 10);
+    const owner = normalizeAccountId(data[2].toJSON() as any);
+    const amount = parseInt(data[3].toString(), 10);
+
+    const itemRes: DestroyItemResult = {
+      success: true,
+      collectionId,
+      itemId,
+      owner,
+      amount,
+    };
+
+    results.push(itemRes);
+    return results;
+  });
+
+  if (!genericResult.success) return [];
+  return results;
+}
+
 export function getTransferResult(api: ApiPromise, events: EventRecord[]): TransferResult {
   for (const {event} of events) {
     if (api.events.common.Transfer.is(event)) {