difftreelog
feat erc nesting tests + some fixes
in: master
5 files changed
pallets/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());
}
pallets/fungible/src/erc.rsdiffbeforeafterboth1// 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}tests/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');
+ });
+ });
+ });
});
tests/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);
tests/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'