git.delta.rocks / unique-network / refs/commits / 008dfe41f9df

difftreelog

feat erc nesting tests + some fixes

Trubnikov Sergey2023-05-10parent: #2dd0a6a.patch.diff
in: master

5 files changed

modifiedpallets/balances-adapter/src/lib.rsdiffbeforeafterboth
--- a/pallets/balances-adapter/src/lib.rs
+++ b/pallets/balances-adapter/src/lib.rs
@@ -104,22 +104,20 @@
 			from: &T::CrossAccountId,
 			nesting_budget: &dyn Budget,
 		) -> Result<u128, DispatchError> {
-			if spender.conv_eq(from) {
-				return Ok(0);
-			}
-
-			if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {
+			if let Some((collection_id, token_id)) =
+				T::CrossTokenAddressMapping::address_to_token(from)
+			{
 				ensure!(
 					<PalletStructure<T>>::check_indirectly_owned(
 						spender.clone(),
-						source.0,
-						source.1,
+						collection_id,
+						token_id,
 						None,
 						nesting_budget
 					)?,
 					<CommonError<T>>::ApprovedValueTooLow,
 				);
-			} else if spender != from {
+			} else if !spender.conv_eq(from) {
 				return Ok(0);
 			}
 
@@ -183,7 +181,7 @@
 			amount: u128,
 			nesting_budget: &dyn Budget,
 		) -> DispatchResultWithPostInfo {
-			let allowance = Self::check_allowed(collection, spender, from, amount, nesting_budget)?;
+			let allowance = Self::check_allowed(spender, from, nesting_budget)?;
 			if allowance < amount {
 				return Err(<CommonError<T>>::ApprovedValueTooLow.into());
 			}
modifiedpallets/fungible/src/erc.rsdiffbeforeafterboth
before · pallets/fungible/src/erc.rs
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! ERC-20 standart support implementation.1819extern crate alloc;20use core::char::{REPLACEMENT_CHARACTER, decode_utf16};21use core::convert::TryInto;22use evm_coder::AbiCoder;23use evm_coder::{abi::AbiType, ToLog, generate_stubgen, solidity_interface, types::*};24use up_data_structs::CollectionMode;25use pallet_common::{26	CollectionHandle,27	erc::{CommonEvmHandler, PrecompileResult, CollectionCall},28	eth::CrossAddress,29	CommonWeightInfo as _,30};31use sp_std::vec::Vec;32use pallet_evm::{account::CrossAccountId, PrecompileHandle};33use pallet_evm_coder_substrate::{34	call, dispatch_to_evm,35	execution::{PreDispatch, Result},36	frontier_contract,37};38use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};39use sp_core::{U256, Get};4041use crate::{42	Allowance, Balance, Config, FungibleHandle, Pallet, TotalSupply, SelfWeightOf,43	weights::WeightInfo, common::CommonWeights,44};4546frontier_contract! {47	macro_rules! FungibleHandle_result {...}48	impl<T: Config> Contract for FungibleHandle<T> {...}49}5051#[derive(ToLog)]52pub enum ERC20Events {53	Transfer {54		#[indexed]55		from: Address,56		#[indexed]57		to: Address,58		value: U256,59	},60	Approval {61		#[indexed]62		owner: Address,63		#[indexed]64		spender: Address,65		value: U256,66	},67}6869#[derive(AbiCoder, Debug)]70pub struct AmountForAddress {71	to: Address,72	amount: U256,73}7475#[solidity_interface(name = ERC20, events(ERC20Events), enum(derive(PreDispatch)), enum_attr(weight), expect_selector = 0x942e8b22)]76impl<T: Config> FungibleHandle<T> {77	fn name(&self) -> Result<String> {78		Ok(decode_utf16(self.name.iter().copied())79			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))80			.collect::<String>())81	}82	fn symbol(&self) -> Result<String> {83		Ok(String::from_utf8_lossy(&self.token_prefix).into())84	}85	fn total_supply(&self) -> Result<U256> {86		self.consume_store_reads(1)?;87		Ok(<TotalSupply<T>>::get(self.id).into())88	}8990	fn decimals(&self) -> Result<u8> {91		Ok(if let CollectionMode::Fungible(decimals) = &self.mode {92			*decimals93		} else {94			unreachable!()95		})96	}97	fn balance_of(&self, owner: Address) -> Result<U256> {98		self.consume_store_reads(1)?;99		let owner = T::CrossAccountId::from_eth(owner);100		let balance = <Balance<T>>::get((self.id, owner));101		Ok(balance.into())102	}103	#[weight(<CommonWeights<T>>::transfer())]104	fn transfer(&mut self, caller: Caller, to: Address, amount: U256) -> Result<bool> {105		let caller = T::CrossAccountId::from_eth(caller);106		let to = T::CrossAccountId::from_eth(to);107		let amount = amount.try_into().map_err(|_| "amount overflow")?;108		let budget = self109			.recorder110			.weight_calls_budget(<StructureWeight<T>>::find_parent());111112		<Pallet<T>>::transfer(self, &caller, &to, amount, &budget).map_err(|_| "transfer error")?;113		Ok(true)114	}115116	#[weight(<CommonWeights<T>>::transfer_from())]117	fn transfer_from(118		&mut self,119		caller: Caller,120		from: Address,121		to: Address,122		amount: U256,123	) -> Result<bool> {124		let caller = T::CrossAccountId::from_eth(caller);125		let from = T::CrossAccountId::from_eth(from);126		let to = T::CrossAccountId::from_eth(to);127		let amount = amount.try_into().map_err(|_| "amount overflow")?;128		let budget = self129			.recorder130			.weight_calls_budget(<StructureWeight<T>>::find_parent());131132		<Pallet<T>>::transfer_from(self, &caller, &from, &to, amount, &budget)133			.map_err(|e| dispatch_to_evm::<T>(e.error))?;134		Ok(true)135	}136	#[weight(<SelfWeightOf<T>>::approve())]137	fn approve(&mut self, caller: Caller, spender: Address, amount: U256) -> Result<bool> {138		let caller = T::CrossAccountId::from_eth(caller);139		let spender = T::CrossAccountId::from_eth(spender);140		let amount = amount.try_into().map_err(|_| "amount overflow")?;141142		<Pallet<T>>::set_allowance(self, &caller, &spender, amount)143			.map_err(dispatch_to_evm::<T>)?;144		Ok(true)145	}146	fn allowance(&self, owner: Address, spender: Address) -> Result<U256> {147		self.consume_store_reads(1)?;148		let owner = T::CrossAccountId::from_eth(owner);149		let spender = T::CrossAccountId::from_eth(spender);150151		Ok(<Allowance<T>>::get((self.id, owner, spender)).into())152	}153}154155#[solidity_interface(name = ERC20Mintable, enum(derive(PreDispatch)), enum_attr(weight))]156impl<T: Config> FungibleHandle<T> {157	/// Mint tokens for `to` account.158	/// @param to account that will receive minted tokens159	/// @param amount amount of tokens to mint160	#[weight(<SelfWeightOf<T>>::create_item())]161	fn mint(&mut self, caller: Caller, to: Address, amount: U256) -> Result<bool> {162		let caller = T::CrossAccountId::from_eth(caller);163		let to = T::CrossAccountId::from_eth(to);164		let amount = amount.try_into().map_err(|_| "amount overflow")?;165		let budget = self166			.recorder167			.weight_calls_budget(<StructureWeight<T>>::find_parent());168		<Pallet<T>>::create_item(self, &caller, (to, amount), &budget)169			.map_err(dispatch_to_evm::<T>)?;170		Ok(true)171	}172}173174#[solidity_interface(name = ERC20UniqueExtensions, enum(derive(PreDispatch)), enum_attr(weight))]175impl<T: Config> FungibleHandle<T>176where177	T::AccountId: From<[u8; 32]>,178{179	/// @dev Function to check the amount of tokens that an owner allowed to a spender.180	/// @param owner crossAddress The address which owns the funds.181	/// @param spender crossAddress The address which will spend the funds.182	/// @return A uint256 specifying the amount of tokens still available for the spender.183	fn allowance_cross(&self, owner: CrossAddress, spender: CrossAddress) -> Result<U256> {184		let owner = owner.into_sub_cross_account::<T>()?;185		let spender = spender.into_sub_cross_account::<T>()?;186187		Ok(<Allowance<T>>::get((self.id, owner, spender)).into())188	}189190	/// @notice A description for the collection.191	fn description(&self) -> String {192		decode_utf16(self.description.iter().copied())193			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))194			.collect::<String>()195	}196197	#[weight(<SelfWeightOf<T>>::create_item())]198	fn mint_cross(&mut self, caller: Caller, to: CrossAddress, amount: U256) -> Result<bool> {199		let caller = T::CrossAccountId::from_eth(caller);200		let to = to.into_sub_cross_account::<T>()?;201		let amount = amount.try_into().map_err(|_| "amount overflow")?;202		let budget = self203			.recorder204			.weight_calls_budget(<StructureWeight<T>>::find_parent());205		<Pallet<T>>::create_item(self, &caller, (to, amount), &budget)206			.map_err(dispatch_to_evm::<T>)?;207		Ok(true)208	}209210	#[weight(<SelfWeightOf<T>>::approve())]211	fn approve_cross(212		&mut self,213		caller: Caller,214		spender: CrossAddress,215		amount: U256,216	) -> Result<bool> {217		let caller = T::CrossAccountId::from_eth(caller);218		let spender = spender.into_sub_cross_account::<T>()?;219		let amount = amount.try_into().map_err(|_| "amount overflow")?;220221		<Pallet<T>>::set_allowance(self, &caller, &spender, amount)222			.map_err(dispatch_to_evm::<T>)?;223		Ok(true)224	}225226	/// Burn tokens from account227	/// @dev Function that burns an `amount` of the tokens of a given account,228	/// deducting from the sender's allowance for said account.229	/// @param from The account whose tokens will be burnt.230	/// @param amount The amount that will be burnt.231	#[solidity(hide)]232	#[weight(<SelfWeightOf<T>>::burn_from())]233	fn burn_from(&mut self, caller: Caller, from: Address, amount: U256) -> Result<bool> {234		let caller = T::CrossAccountId::from_eth(caller);235		let from = T::CrossAccountId::from_eth(from);236		let amount = amount.try_into().map_err(|_| "amount overflow")?;237		let budget = self238			.recorder239			.weight_calls_budget(<StructureWeight<T>>::find_parent());240241		<Pallet<T>>::burn_from(self, &caller, &from, amount, &budget)242			.map_err(dispatch_to_evm::<T>)?;243		Ok(true)244	}245246	/// Burn tokens from account247	/// @dev Function that burns an `amount` of the tokens of a given account,248	/// deducting from the sender's allowance for said account.249	/// @param from The account whose tokens will be burnt.250	/// @param amount The amount that will be burnt.251	#[weight(<SelfWeightOf<T>>::burn_from())]252	fn burn_from_cross(253		&mut self,254		caller: Caller,255		from: CrossAddress,256		amount: U256,257	) -> Result<bool> {258		let caller = T::CrossAccountId::from_eth(caller);259		let from = from.into_sub_cross_account::<T>()?;260		let amount = amount.try_into().map_err(|_| "amount overflow")?;261		let budget = self262			.recorder263			.weight_calls_budget(<StructureWeight<T>>::find_parent());264265		<Pallet<T>>::burn_from(self, &caller, &from, amount, &budget)266			.map_err(dispatch_to_evm::<T>)?;267		Ok(true)268	}269270	/// Mint tokens for multiple accounts.271	/// @param amounts array of pairs of account address and amount272	#[weight(<SelfWeightOf<T>>::create_multiple_items_ex(amounts.len() as u32))]273	fn mint_bulk(&mut self, caller: Caller, amounts: Vec<AmountForAddress>) -> Result<bool> {274		let caller = T::CrossAccountId::from_eth(caller);275		let budget = self276			.recorder277			.weight_calls_budget(<StructureWeight<T>>::find_parent());278		let amounts = amounts279			.into_iter()280			.map(|AmountForAddress { to, amount }| {281				Ok((282					T::CrossAccountId::from_eth(to),283					amount.try_into().map_err(|_| "amount overflow")?,284				))285			})286			.collect::<Result<_>>()?;287288		<Pallet<T>>::create_multiple_items(self, &caller, amounts, &budget)289			.map_err(dispatch_to_evm::<T>)?;290		Ok(true)291	}292293	#[weight(<CommonWeights<T>>::transfer())]294	fn transfer_cross(&mut self, caller: Caller, to: CrossAddress, amount: U256) -> Result<bool> {295		let caller = T::CrossAccountId::from_eth(caller);296		let to = to.into_sub_cross_account::<T>()?;297		let amount = amount.try_into().map_err(|_| "amount overflow")?;298		let budget = self299			.recorder300			.weight_calls_budget(<StructureWeight<T>>::find_parent());301302		<Pallet<T>>::transfer(self, &caller, &to, amount, &budget).map_err(|_| "transfer error")?;303		Ok(true)304	}305306	#[weight(<CommonWeights<T>>::transfer_from())]307	fn transfer_from_cross(308		&mut self,309		caller: Caller,310		from: CrossAddress,311		to: CrossAddress,312		amount: U256,313	) -> Result<bool> {314		let caller = T::CrossAccountId::from_eth(caller);315		let from = from.into_sub_cross_account::<T>()?;316		let to = to.into_sub_cross_account::<T>()?;317		let amount = amount.try_into().map_err(|_| "amount overflow")?;318		let budget = self319			.recorder320			.weight_calls_budget(<StructureWeight<T>>::find_parent());321322		<Pallet<T>>::transfer_from(self, &caller, &from, &to, amount, &budget)323			.map_err(|e| dispatch_to_evm::<T>(e.error))?;324		Ok(true)325	}326327	/// @notice Returns collection helper contract address328	fn collection_helper_address(&self) -> Result<Address> {329		Ok(T::ContractAddress::get())330	}331332	/// @notice Balance of account333	/// @param owner An cross address for whom to query the balance334	/// @return The number of fingibles owned by `owner`, possibly zero335	fn balance_of_cross(&self, owner: CrossAddress) -> Result<U256> {336		self.consume_store_reads(1)?;337		let balance = <Balance<T>>::get((self.id, owner.into_sub_cross_account::<T>()?));338		Ok(balance.into())339	}340}341342#[solidity_interface(343	name = UniqueFungible,344	is(345		ERC20,346		ERC20Mintable,347		ERC20UniqueExtensions,348		Collection(via(common_mut returns CollectionHandle<T>)),349	),350	enum(derive(PreDispatch))351)]352impl<T: Config> FungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}353354generate_stubgen!(gen_impl, UniqueFungibleCall<()>, true);355generate_stubgen!(gen_iface, UniqueFungibleCall<()>, false);356357impl<T: Config> CommonEvmHandler for FungibleHandle<T>358where359	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,360{361	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueFungible.raw");362363	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {364		call::<T, UniqueFungibleCall<T>, _, _>(handle, self)365	}366}
modifiedtests/src/eth/nesting/nest.test.tsdiffbeforeafterboth
--- a/tests/src/eth/nesting/nest.test.ts
+++ b/tests/src/eth/nesting/nest.test.ts
@@ -187,4 +187,80 @@
         .call()).to.be.rejectedWith('SourceCollectionIsNotAllowedToNest');
     });
   });
+
+  describe('Fungible', () => {
+    async function createFungibleCollection(helper: EthUniqueHelper, owner: string, mode: 'ft' | 'native ft') {
+      if (mode === 'ft') {
+        const {collectionAddress} = await helper.eth.createFungibleCollection(owner, '', 18, '', '');
+        const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner);
+        await contract.methods.mint(owner, 100n).send({from: owner});
+        return {collectionAddress, contract};
+      }
+
+      // native ft
+      const collectionAddress = helper.ethAddress.fromCollectionId(0);
+      const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner);
+      return {collectionAddress, contract};
+    }
+
+    [
+      {mode: 'ft' as const},
+      {mode: 'native ft' as const},
+    ].map(testCase => {
+      itEth(`Allow nest [${testCase.mode}]`, async ({helper}) => {
+        const owner = await helper.eth.createAccountWithBalance(donor);
+        const {collectionId: targetCollectionId, contract: targetContract} = await createNestingCollection(helper, owner);
+        const {contract: ftContract} = await createFungibleCollection(helper, owner, testCase.mode);
+
+        const mintingTargetTokenIdResult = await targetContract.methods.mint(owner).send({from: owner});
+        const targetTokenId = mintingTargetTokenIdResult.events.Transfer.returnValues.tokenId;
+        const targetTokenAddress = helper.ethAddress.fromTokenId(targetCollectionId, targetTokenId);
+
+        await ftContract.methods.transfer(targetTokenAddress, 10n).send({from: owner});
+        expect(await ftContract.methods.balanceOf(targetTokenAddress).call({from: owner})).to.be.equal('10');
+      });
+    });
+
+    [
+      {mode: 'ft' as const},
+      {mode: 'native ft' as const},
+    ].map(testCase => {
+      itEth(`Allow partial/full unnest [${testCase.mode}]`, async ({helper}) => {
+        const owner = await helper.eth.createAccountWithBalance(donor);
+        const {collectionId: targetCollectionId, contract: targetContract} = await createNestingCollection(helper, owner);
+        const {contract: ftContract} = await createFungibleCollection(helper, owner, testCase.mode);
+
+        const mintingTargetTokenIdResult = await targetContract.methods.mint(owner).send({from: owner});
+        const targetTokenId = mintingTargetTokenIdResult.events.Transfer.returnValues.tokenId;
+        const targetTokenAddress = helper.ethAddress.fromTokenId(targetCollectionId, targetTokenId);
+
+        await ftContract.methods.transfer(targetTokenAddress, 10n).send({from: owner});
+
+        await ftContract.methods.transferFrom(targetTokenAddress, owner, 5n).send({from: owner});
+        expect(await ftContract.methods.balanceOf(targetTokenAddress).call({from: owner})).to.be.equal('5');
+
+        await ftContract.methods.transferFrom(targetTokenAddress, owner, 5n).send({from: owner});
+        expect(await ftContract.methods.balanceOf(targetTokenAddress).call({from: owner})).to.be.equal('0');
+      });
+    });
+
+    [
+      {mode: 'ft' as const},
+      {mode: 'native ft' as const},
+    ].map(testCase => {
+      itEth(`Disallow nest into collection without nesting permission [${testCase.mode}]`, async ({helper}) => {
+        const owner = await helper.eth.createAccountWithBalance(donor);
+        const {collectionId: targetCollectionId, contract: targetContract} = await createNestingCollection(helper, owner);
+        await targetContract.methods.setCollectionNesting(false).send({from: owner});
+
+        const {contract: ftContract} = await createFungibleCollection(helper, owner, testCase.mode);
+
+        const mintingTargetTokenIdResult = await targetContract.methods.mint(owner).send({from: owner});
+        const targetTokenId = mintingTargetTokenIdResult.events.Transfer.returnValues.tokenId;
+        const targetTokenAddress = helper.ethAddress.fromTokenId(targetCollectionId, targetTokenId);
+
+        await expect(ftContract.methods.transfer(targetTokenAddress, 10n).call({from: owner})).to.be.rejectedWith('UserIsNotAllowedToNest');
+      });
+    });
+  });
 });
modifiedtests/src/nativeFungible.test.tsdiffbeforeafterboth
--- a/tests/src/nativeFungible.test.ts
+++ b/tests/src/nativeFungible.test.ts
@@ -207,7 +207,7 @@
     )).to.be.rejectedWith('BadOrigin');
   });
 
-  itSub.only('Nest into NFT token()', async ({helper}) => {
+  itSub('Nest into NFT token()', async ({helper}) => {
     const nftCollection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
     const targetToken = await nftCollection.mintToken(alice);
 
modifiedtests/src/sub/nesting/unnesting.negative.test.tsdiffbeforeafterboth
--- a/tests/src/sub/nesting/unnesting.negative.test.ts
+++ b/tests/src/sub/nesting/unnesting.negative.test.ts
@@ -58,7 +58,7 @@
     {mode: md.mode, restrictedMode: true},
     {mode: md.mode, restrictedMode: false},
   ].map(testCase => {
-    itSub.only(`Fungible: disallows a non-Owner to unnest someone else's token [${testCase.mode}${testCase.restrictedMode ? ' (Restricted nesting)' : ''}]`, async ({helper}) => {
+    itSub(`Fungible: disallows a non-Owner to unnest someone else's token [${testCase.mode}${testCase.restrictedMode ? ' (Restricted nesting)' : ''}]`, async ({helper}) => {
       const collectionNFT = await helper.nft.mintCollection(alice);
       const collectionFT = await (
         testCase.mode === 'ft'