--- a/Cargo.lock +++ b/Cargo.lock @@ -6361,7 +6361,7 @@ [[package]] name = "pallet-fungible" -version = "0.1.9" +version = "0.1.10" dependencies = [ "evm-coder", "frame-benchmarking", @@ -6774,7 +6774,7 @@ [[package]] name = "pallet-refungible" -version = "0.2.12" +version = "0.2.13" dependencies = [ "evm-coder", "frame-benchmarking", --- a/pallets/common/src/eth.rs +++ b/pallets/common/src/eth.rs @@ -18,7 +18,10 @@ use alloc::format; use sp_std::{vec, vec::Vec}; -use evm_coder::{AbiCoder, types::Address}; +use evm_coder::{ + AbiCoder, + types::{Address, String}, +}; pub use pallet_evm::{Config, account::CrossAccountId}; use sp_core::{H160, U256}; use up_data_structs::CollectionId; @@ -390,6 +393,16 @@ } } +/// Data for creation token with uri. +#[derive(Debug, AbiCoder)] +pub struct TokenUri { + /// Id of new token. + pub id: U256, + + /// Uri of new token. + pub uri: String, +} + /// Nested collections. #[derive(Debug, Default, AbiCoder)] pub struct CollectionNesting { --- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -66,10 +66,11 @@ ensure, traits::{Imbalance, Get, Currency, WithdrawReasons, ExistenceRequirement}, dispatch::Pays, - transactional, + transactional, fail, }; use pallet_evm::GasWeightMapping; use up_data_structs::{ + AccessMode, COLLECTION_NUMBER_LIMIT, Collection, RpcCollection, @@ -124,6 +125,8 @@ pub use pallet::*; use sp_core::H160; use sp_runtime::{ArithmeticError, DispatchError, DispatchResult}; + +use crate::erc::CollectionHelpersEvents; #[cfg(feature = "runtime-benchmarks")] pub mod benchmarking; pub mod dispatch; @@ -1264,6 +1267,133 @@ Ok(()) } + /// A batch operation to add, edit or remove properties for a token. + /// It sets or removes a token's properties according to + /// `properties_updates` contents: + /// * sets a property under the with the value provided `(, Some())` + /// * removes a property under the if the value is `None` `(, None)`. + /// + /// - `nesting_budget`: Limit for searching parents in-depth to check ownership. + /// - `is_token_create`: Indicates that method is called during token initialization. + /// Allows to bypass ownership check. + /// + /// All affected properties should have `mutable` permission + /// to be **deleted** or to be **set more than once**, + /// and the sender should have permission to edit those properties. + /// + /// This function fires an event for each property change. + /// In case of an error, all the changes (including the events) will be reverted + /// since the function is transactional. + pub fn modify_token_properties( + collection: &CollectionHandle, + sender: &T::CrossAccountId, + token_id: TokenId, + properties_updates: impl Iterator)>, + is_token_create: bool, + mut stored_properties: Properties, + is_token_owner: impl Fn() -> Result, + set_token_properties: impl FnOnce(Properties), + ) -> DispatchResult { + let is_collection_admin = collection.is_owner_or_admin(sender); + let permissions = Self::property_permissions(collection.id); + + let mut token_owner_result = None; + let mut is_token_owner = || -> Result { + *token_owner_result.get_or_insert_with(&is_token_owner) + }; + + for (key, value) in properties_updates { + let permission = permissions + .get(&key) + .cloned() + .unwrap_or_else(PropertyPermission::none); + + let is_property_exists = stored_properties.get(&key).is_some(); + + match permission { + PropertyPermission { mutable: false, .. } if is_property_exists => { + return Err(>::NoPermission.into()); + } + + PropertyPermission { + collection_admin, + token_owner, + .. + } => { + //TODO: investigate threats during public minting. + let is_token_create = + is_token_create && (collection_admin || token_owner) && value.is_some(); + if !(is_token_create + || (collection_admin && is_collection_admin) + || (token_owner && is_token_owner()?)) + { + fail!(>::NoPermission); + } + } + } + + match value { + Some(value) => { + stored_properties + .try_set(key.clone(), value) + .map_err(>::from)?; + + Self::deposit_event(Event::TokenPropertySet(collection.id, token_id, key)); + } + None => { + stored_properties.remove(&key).map_err(>::from)?; + + Self::deposit_event(Event::TokenPropertyDeleted(collection.id, token_id, key)); + } + } + + >::deposit_log( + CollectionHelpersEvents::TokenChanged { + collection_id: eth::collection_id_to_address(collection.id), + token_id: token_id.into(), + } + .to_log(T::ContractAddress::get()), + ); + } + + set_token_properties(stored_properties); + + Ok(()) + } + + /// Sets or unsets the approval of a given operator. + /// + /// The `operator` is allowed to transfer all token pieces of the `owner` on their behalf. + /// - `owner`: Token owner + /// - `operator`: Operator + /// - `approve`: Should operator status be granted or revoked? + pub fn set_allowance_for_all( + collection: &CollectionHandle, + owner: &T::CrossAccountId, + operator: &T::CrossAccountId, + approve: bool, + set_allowance: impl FnOnce(), + log: evm_coder::ethereum::Log, + ) -> DispatchResult { + if collection.permissions.access() == AccessMode::AllowList { + collection.check_allowlist(owner)?; + collection.check_allowlist(operator)?; + } + + Self::ensure_correct_receiver(operator)?; + + set_allowance(); + + >::deposit_log(log); + Self::deposit_event(Event::ApprovedForAll( + collection.id, + owner.clone(), + operator.clone(), + approve, + )); + Ok(()) + } + /// Set collection property. /// /// * `collection` - Collection handler. --- a/pallets/fungible/CHANGELOG.md +++ b/pallets/fungible/CHANGELOG.md @@ -4,6 +4,12 @@ +## [0.1.10] - 2023-02-01 + +### Added + +- The functions `allowanceCross` to `ERC20UniqueExtensions` interface. + ## [0.1.9] - 2022-12-01 ### Added --- a/pallets/fungible/Cargo.toml +++ b/pallets/fungible/Cargo.toml @@ -2,7 +2,7 @@ edition = "2021" license = "GPLv3" name = "pallet-fungible" -version = "0.1.9" +version = "0.1.10" [dependencies] # Note: `package = "parity-scale-codec"` must be supplied since the `Encode` macro searches for it. --- a/pallets/fungible/src/erc.rs +++ b/pallets/fungible/src/erc.rs @@ -19,6 +19,7 @@ extern crate alloc; use core::char::{REPLACEMENT_CHARACTER, decode_utf16}; use core::convert::TryInto; +use evm_coder::AbiCoder; use evm_coder::{ abi::AbiType, ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*, weight, @@ -27,6 +28,7 @@ use pallet_common::{ CollectionHandle, erc::{CommonEvmHandler, PrecompileResult, CollectionCall}, + eth::CrossAddress, }; use sp_std::vec::Vec; use pallet_evm::{account::CrossAccountId, PrecompileHandle}; @@ -57,6 +59,12 @@ }, } +#[derive(AbiCoder, Debug)] +pub struct AmountForAddress { + to: Address, + amount: U256, +} + #[solidity_interface(name = ERC20, events(ERC20Events), expect_selector = 0x942e8b22)] impl FungibleHandle { fn name(&self) -> Result { @@ -161,6 +169,17 @@ where T::AccountId: From<[u8; 32]>, { + /// @dev Function to check the amount of tokens that an owner allowed to a spender. + /// @param owner crossAddress The address which owns the funds. + /// @param spender crossAddress The address which will spend the funds. + /// @return A uint256 specifying the amount of tokens still available for the spender. + fn allowance_cross(&self, owner: CrossAddress, spender: CrossAddress) -> Result { + let owner = owner.into_sub_cross_account::()?; + let spender = spender.into_sub_cross_account::()?; + + Ok(>::get((self.id, owner, spender)).into()) + } + /// @notice A description for the collection. fn description(&self) -> Result { Ok(decode_utf16(self.description.iter().copied()) @@ -169,12 +188,7 @@ } #[weight(>::create_item())] - fn mint_cross( - &mut self, - caller: Caller, - to: pallet_common::eth::CrossAddress, - amount: U256, - ) -> Result { + fn mint_cross(&mut self, caller: Caller, to: CrossAddress, amount: U256) -> Result { let caller = T::CrossAccountId::from_eth(caller); let to = to.into_sub_cross_account::()?; let amount = amount.try_into().map_err(|_| "amount overflow")?; @@ -190,7 +204,7 @@ fn approve_cross( &mut self, caller: Caller, - spender: pallet_common::eth::CrossAddress, + spender: CrossAddress, amount: U256, ) -> Result { let caller = T::CrossAccountId::from_eth(caller); @@ -231,7 +245,7 @@ fn burn_from_cross( &mut self, caller: Caller, - from: pallet_common::eth::CrossAddress, + from: CrossAddress, amount: U256, ) -> Result { let caller = T::CrossAccountId::from_eth(caller); @@ -249,14 +263,14 @@ /// Mint tokens for multiple accounts. /// @param amounts array of pairs of account address and amount #[weight(>::create_multiple_items_ex(amounts.len() as u32))] - fn mint_bulk(&mut self, caller: Caller, amounts: Vec<(Address, U256)>) -> Result { + fn mint_bulk(&mut self, caller: Caller, amounts: Vec) -> Result { let caller = T::CrossAccountId::from_eth(caller); let budget = self .recorder .weight_calls_budget(>::find_parent()); let amounts = amounts .into_iter() - .map(|(to, amount)| { + .map(|AmountForAddress { to, amount }| { Ok(( T::CrossAccountId::from_eth(to), amount.try_into().map_err(|_| "amount overflow")?, @@ -270,12 +284,7 @@ } #[weight(>::transfer())] - fn transfer_cross( - &mut self, - caller: Caller, - to: pallet_common::eth::CrossAddress, - amount: U256, - ) -> Result { + fn transfer_cross(&mut self, caller: Caller, to: CrossAddress, amount: U256) -> Result { let caller = T::CrossAccountId::from_eth(caller); let to = to.into_sub_cross_account::()?; let amount = amount.try_into().map_err(|_| "amount overflow")?; @@ -291,8 +300,8 @@ fn transfer_from_cross( &mut self, caller: Caller, - from: pallet_common::eth::CrossAddress, - to: pallet_common::eth::CrossAddress, + from: CrossAddress, + to: CrossAddress, amount: U256, ) -> Result { let caller = T::CrossAccountId::from_eth(caller); --- a/pallets/fungible/src/lib.rs +++ b/pallets/fungible/src/lib.rs @@ -165,8 +165,8 @@ pub type Allowance = StorageNMap< Key = ( Key, - Key, - Key, + Key, // Owner + Key, // Spender ), Value = u128, QueryKind = ValueQuery, --- a/pallets/fungible/src/stubs/UniqueFungible.sol +++ b/pallets/fungible/src/stubs/UniqueFungible.sol @@ -511,8 +511,22 @@ bytes value; } -/// @dev the ERC-165 identifier for this interface is 0x65789571 +/// @dev the ERC-165 identifier for this interface is 0x85d7dea6 contract ERC20UniqueExtensions is Dummy, ERC165 { + /// @dev Function to check the amount of tokens that an owner allowed to a spender. + /// @param owner crossAddress The address which owns the funds. + /// @param spender crossAddress The address which will spend the funds. + /// @return A uint256 specifying the amount of tokens still available for the spender. + /// @dev EVM selector for this function is: 0xe0af4bd7, + /// or in textual repr: allowanceCross((address,uint256),(address,uint256)) + function allowanceCross(CrossAddress memory owner, CrossAddress memory spender) public view returns (uint256) { + require(false, stub_error); + owner; + spender; + dummy; + return 0; + } + /// @notice A description for the collection. /// @dev EVM selector for this function is: 0x7284e416, /// or in textual repr: description() @@ -576,7 +590,7 @@ /// @param amounts array of pairs of account address and amount /// @dev EVM selector for this function is: 0x1acf2d55, /// or in textual repr: mintBulk((address,uint256)[]) - function mintBulk(Tuple9[] memory amounts) public returns (bool) { + function mintBulk(AmountForAddress[] memory amounts) public returns (bool) { require(false, stub_error); amounts; dummy = 0; @@ -618,10 +632,9 @@ } } -/// @dev anonymous struct -struct Tuple9 { - address field_0; - uint256 field_1; +struct AmountForAddress { + address to; + uint256 amount; } /// @dev the ERC-165 identifier for this interface is 0x40c10f19 --- a/pallets/nonfungible/src/erc.rs +++ b/pallets/nonfungible/src/erc.rs @@ -38,7 +38,7 @@ use pallet_common::{ CollectionHandle, CollectionPropertyPermissions, CommonCollectionOperations, erc::{CommonEvmHandler, PrecompileResult, CollectionCall, static_property::key}, - eth, + eth::{self, TokenUri}, }; use pallet_evm::{account::CrossAccountId, PrecompileHandle}; use pallet_evm_coder_substrate::call; @@ -948,7 +948,7 @@ &mut self, caller: Caller, to: Address, - tokens: Vec<(U256, String)>, + tokens: Vec, ) -> Result { let key = key::url(); let caller = T::CrossAccountId::from_eth(caller); @@ -961,7 +961,7 @@ .weight_calls_budget(>::find_parent()); let mut data = Vec::with_capacity(tokens.len()); - for (id, token_uri) in tokens { + for TokenUri { id, uri } in tokens { let id: u32 = id.try_into().map_err(|_| "token id overflow")?; if id != expected_index { return Err("item id should be next".into()); @@ -972,7 +972,7 @@ properties .try_push(Property { key: key.clone(), - value: token_uri + value: uri .into_bytes() .try_into() .map_err(|_| "token uri is too long")?, --- a/pallets/nonfungible/src/lib.rs +++ b/pallets/nonfungible/src/lib.rs @@ -101,18 +101,18 @@ }; use up_data_structs::{ AccessMode, CollectionId, CollectionFlags, CustomDataLimit, TokenId, CreateCollectionData, - CreateNftExData, mapping::TokenAddressMapping, budget::Budget, Property, PropertyPermission, - PropertyKey, PropertyValue, PropertyKeyPermission, Properties, PropertyScope, TrySetProperty, - TokenChild, AuxPropertyValue, PropertiesPermissionMap, + CreateNftExData, mapping::TokenAddressMapping, budget::Budget, Property, PropertyKey, + PropertyValue, PropertyKeyPermission, Properties, PropertyScope, TrySetProperty, TokenChild, + AuxPropertyValue, PropertiesPermissionMap, }; use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm}; use pallet_common::{ Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle, - eth::collection_id_to_address, erc::CollectionHelpersEvents, + eth::collection_id_to_address, }; use pallet_structure::{Pallet as PalletStructure, Error as StructureError}; use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder}; -use sp_core::{H160, Get}; +use sp_core::H160; use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome}; use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap}; use core::ops::Deref; @@ -578,10 +578,6 @@ } /// A batch operation to add, edit or remove properties for a token. - /// It sets or removes a token's properties according to - /// `properties_updates` contents: - /// * sets a property under the with the value provided `(, Some())` - /// * removes a property under the if the value is `None` `(, None)`. /// /// - `nesting_budget`: Limit for searching parents in-depth to check ownership. /// - `is_token_create`: Indicates that method is called during token initialization. @@ -603,97 +599,30 @@ is_token_create: bool, nesting_budget: &dyn Budget, ) -> DispatchResult { - let mut collection_admin_status = None; - let mut token_owner_result = None; - - let mut is_collection_admin = - || *collection_admin_status.get_or_insert_with(|| collection.is_owner_or_admin(sender)); + let is_token_owner = || { + let is_owned = >::check_indirectly_owned( + sender.clone(), + collection.id, + token_id, + None, + nesting_budget, + )?; - let mut is_token_owner = || { - *token_owner_result.get_or_insert_with(|| -> Result { - let is_owned = >::check_indirectly_owned( - sender.clone(), - collection.id, - token_id, - None, - nesting_budget, - )?; - - Ok(is_owned) - }) + Ok(is_owned) }; - - let mut stored_properties = >::get((collection.id, token_id)); - let permissions = >::property_permissions(collection.id); - - for (key, value) in properties_updates { - let permission = permissions - .get(&key) - .cloned() - .unwrap_or_else(PropertyPermission::none); - - let is_property_exists = stored_properties.get(&key).is_some(); - - match permission { - PropertyPermission { mutable: false, .. } if is_property_exists => { - return Err(>::NoPermission.into()); - } - - PropertyPermission { - collection_admin, - token_owner, - .. - } => { - //TODO: investigate threats during public minting. - if is_token_create && (collection_admin || token_owner) && value.is_some() { - // Pass - } else if collection_admin && is_collection_admin() { - // Pass - } else if token_owner && is_token_owner()? { - // Pass - } else { - fail!(>::NoPermission); - } - } - } - match value { - Some(value) => { - stored_properties - .try_set(key.clone(), value) - .map_err(>::from)?; - - >::deposit_event(CommonEvent::TokenPropertySet( - collection.id, - token_id, - key, - )); - } - None => { - stored_properties - .remove(&key) - .map_err(>::from)?; - - >::deposit_event(CommonEvent::TokenPropertyDeleted( - collection.id, - token_id, - key, - )); - } - } - - >::deposit_log( - CollectionHelpersEvents::TokenChanged { - collection_id: collection_id_to_address(collection.id), - token_id: token_id.into(), - } - .to_log(T::ContractAddress::get()), - ); - } - - >::set((collection.id, token_id), stored_properties); + let stored_properties = >::get((collection.id, token_id)); - Ok(()) + >::modify_token_properties( + collection, + sender, + token_id, + properties_updates, + is_token_create, + stored_properties, + is_token_owner, + |properties| >::set((collection.id, token_id), properties), + ) } /// Batch operation to add or edit properties for the token @@ -1418,31 +1347,19 @@ operator: &T::CrossAccountId, approve: bool, ) -> DispatchResult { - if collection.permissions.access() == AccessMode::AllowList { - collection.check_allowlist(owner)?; - collection.check_allowlist(operator)?; - } - - >::ensure_correct_receiver(operator)?; - - // ========= - - >::insert((collection.id, owner, operator), approve); - >::deposit_log( + >::set_allowance_for_all( + collection, + owner, + operator, + approve, + || >::insert((collection.id, owner, operator), approve), ERC721Events::ApprovalForAll { owner: *owner.as_eth(), operator: *operator.as_eth(), approved: approve, } .to_log(collection_id_to_address(collection.id)), - ); - >::deposit_event(CommonEvent::ApprovedForAll( - collection.id, - owner.clone(), - operator.clone(), - approve, - )); - Ok(()) + ) } /// Tells whether the given `owner` approves the `operator`. --- a/pallets/nonfungible/src/stubs/UniqueNFT.sol +++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol @@ -949,7 +949,7 @@ // /// @param tokens array of pairs of token ID and token URI for minted tokens // /// @dev EVM selector for this function is: 0x36543006, // /// or in textual repr: mintBulkWithTokenURI(address,(uint256,string)[]) - // function mintBulkWithTokenURI(address to, Tuple15[] memory tokens) public returns (bool) { + // function mintBulkWithTokenURI(address to, TokenUri[] memory tokens) public returns (bool) { // require(false, stub_error); // to; // tokens; @@ -981,10 +981,12 @@ } } -/// @dev anonymous struct -struct Tuple15 { - uint256 field_0; - string field_1; +/// Data for creation token with uri. +struct TokenUri { + /// Id of new token. + uint256 id; + /// Uri of new token. + string uri; } /// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension --- a/pallets/refungible/CHANGELOG.md +++ b/pallets/refungible/CHANGELOG.md @@ -4,6 +4,12 @@ +## [0.2.13] - 2023-02-01 + +### Added + +- The functions `allowanceCross` to `ERC20UniqueExtensions` interface. + ## [0.2.12] - 2023-01-20 ### Fixed --- a/pallets/refungible/Cargo.toml +++ b/pallets/refungible/Cargo.toml @@ -2,7 +2,7 @@ edition = "2021" license = "GPLv3" name = "pallet-refungible" -version = "0.2.12" +version = "0.2.13" [dependencies] # Note: `package = "parity-scale-codec"` must be supplied since the `Encode` macro searches for it. --- a/pallets/refungible/src/erc.rs +++ b/pallets/refungible/src/erc.rs @@ -34,7 +34,7 @@ CollectionHandle, CollectionPropertyPermissions, CommonCollectionOperations, Error as CommonError, erc::{CommonEvmHandler, CollectionCall, static_property::key}, - eth, + eth::{self, TokenUri}, }; use pallet_evm::{account::CrossAccountId, PrecompileHandle}; use pallet_evm_coder_substrate::{call, dispatch_to_evm}; @@ -999,7 +999,7 @@ &mut self, caller: Caller, to: Address, - tokens: Vec<(U256, String)>, + tokens: Vec, ) -> Result { let key = key::url(); let caller = T::CrossAccountId::from_eth(caller); @@ -1017,7 +1017,7 @@ .collect::>() .try_into() .unwrap(); - for (id, token_uri) in tokens { + for TokenUri { id, uri } in tokens { let id: u32 = id.try_into().map_err(|_| "token id overflow")?; if id != expected_index { return Err("item id should be next".into()); @@ -1028,7 +1028,7 @@ properties .try_push(Property { key: key.clone(), - value: token_uri + value: uri .into_bytes() .try_into() .map_err(|_| "token uri is too long")?, --- a/pallets/refungible/src/erc_token.rs +++ b/pallets/refungible/src/erc_token.rs @@ -31,7 +31,7 @@ use pallet_common::{ CommonWeightInfo, erc::{CommonEvmHandler, PrecompileResult}, - eth::collection_id_to_address, + eth::{collection_id_to_address, CrossAddress}, }; use pallet_evm::{account::CrossAccountId, PrecompileHandle}; use pallet_evm_coder_substrate::{call, dispatch_to_evm, WithRecorder}; @@ -203,6 +203,17 @@ where T::AccountId: From<[u8; 32]>, { + /// @dev Function to check the amount of tokens that an owner allowed to a spender. + /// @param owner crossAddress The address which owns the funds. + /// @param spender crossAddress The address which will spend the funds. + /// @return A uint256 specifying the amount of tokens still available for the spender. + fn allowance_cross(&self, owner: CrossAddress, spender: CrossAddress) -> Result { + let owner = owner.into_sub_cross_account::()?; + let spender = spender.into_sub_cross_account::()?; + + Ok(>::get((self.id, self.1, owner, spender)).into()) + } + /// @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. @@ -230,7 +241,7 @@ fn burn_from_cross( &mut self, caller: Caller, - from: pallet_common::eth::CrossAddress, + from: CrossAddress, amount: U256, ) -> Result { let caller = T::CrossAccountId::from_eth(caller); @@ -256,7 +267,7 @@ fn approve_cross( &mut self, caller: Caller, - spender: pallet_common::eth::CrossAddress, + spender: CrossAddress, amount: U256, ) -> Result { let caller = T::CrossAccountId::from_eth(caller); @@ -283,12 +294,7 @@ /// @param to The crossaccount to transfer to. /// @param amount The amount to be transferred. #[weight(>::transfer())] - fn transfer_cross( - &mut self, - caller: Caller, - to: pallet_common::eth::CrossAddress, - amount: U256, - ) -> Result { + fn transfer_cross(&mut self, caller: Caller, to: CrossAddress, amount: U256) -> Result { let caller = T::CrossAccountId::from_eth(caller); let to = to.into_sub_cross_account::()?; let amount = amount.try_into().map_err(|_| "amount overflow")?; @@ -309,8 +315,8 @@ fn transfer_from_cross( &mut self, caller: Caller, - from: pallet_common::eth::CrossAddress, - to: pallet_common::eth::CrossAddress, + from: CrossAddress, + to: CrossAddress, amount: U256, ) -> Result { let caller = T::CrossAccountId::from_eth(caller); --- a/pallets/refungible/src/lib.rs +++ b/pallets/refungible/src/lib.rs @@ -92,22 +92,22 @@ use core::ops::Deref; use evm_coder::ToLog; -use frame_support::{ensure, fail, storage::with_transaction, transactional}; +use frame_support::{ensure, storage::with_transaction, transactional}; use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm}; use pallet_evm_coder_substrate::WithRecorder; use pallet_common::{ CommonCollectionOperations, Error as CommonError, eth::collection_id_to_address, - Event as CommonEvent, Pallet as PalletCommon, erc::CollectionHelpersEvents, + Event as CommonEvent, Pallet as PalletCommon, }; use pallet_structure::Pallet as PalletStructure; -use sp_core::{Get, H160}; +use sp_core::H160; use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome}; use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap}; use up_data_structs::{ AccessMode, budget::Budget, CollectionId, CollectionFlags, CreateCollectionData, mapping::TokenAddressMapping, MAX_REFUNGIBLE_PIECES, Property, PropertyKey, - PropertyKeyPermission, PropertyPermission, PropertyScope, PropertyValue, TokenId, - TrySetProperty, PropertiesPermissionMap, CreateRefungibleExMultipleOwners, TokenOwnerError, + PropertyKeyPermission, PropertyScope, PropertyValue, TokenId, TrySetProperty, + PropertiesPermissionMap, CreateRefungibleExMultipleOwners, TokenOwnerError, }; pub use pallet::*; @@ -240,13 +240,13 @@ QueryKind = ValueQuery, >; - /// Operator set by a wallet owner that could perform certain transactions on all tokens in the wallet. + /// Spender set by a wallet owner that could perform certain transactions on all tokens in the wallet. #[pallet::storage] pub type CollectionAllowance = StorageNMap< Key = ( Key, - Key, - Key, + Key, // Owner + Key, // Spender ), Value = bool, QueryKind = ValueQuery, @@ -541,7 +541,6 @@ is_token_create: bool, nesting_budget: &dyn Budget, ) -> DispatchResult { - let is_collection_admin = || collection.is_owner_or_admin(sender); let is_token_owner = || -> Result { let balance = collection.balance(sender.clone(), token_id); let total_pieces: u128 = @@ -560,77 +559,19 @@ Ok(is_bundle_owner) }; - - let mut stored_properties = >::get((collection.id, token_id)); - let permissions = >::property_permissions(collection.id); - - for (key, value) in properties_updates { - let permission = permissions - .get(&key) - .cloned() - .unwrap_or_else(PropertyPermission::none); - - let is_property_exists = stored_properties.get(&key).is_some(); - - match permission { - PropertyPermission { mutable: false, .. } if is_property_exists => { - return Err(>::NoPermission.into()); - } - PropertyPermission { - collection_admin, - token_owner, - .. - } => { - //TODO: investigate threats during public minting. - let is_token_create = - is_token_create && (collection_admin || token_owner) && value.is_some(); - if !(is_token_create - || (collection_admin && is_collection_admin()) - || (token_owner && is_token_owner()?)) - { - fail!(>::NoPermission); - } - } - } - - match value { - Some(value) => { - stored_properties - .try_set(key.clone(), value) - .map_err(>::from)?; - - >::deposit_event(CommonEvent::TokenPropertySet( - collection.id, - token_id, - key, - )); - } - None => { - stored_properties - .remove(&key) - .map_err(>::from)?; - - >::deposit_event(CommonEvent::TokenPropertyDeleted( - collection.id, - token_id, - key, - )); - } - } - - >::deposit_log( - CollectionHelpersEvents::TokenChanged { - collection_id: collection_id_to_address(collection.id), - token_id: token_id.into(), - } - .to_log(T::ContractAddress::get()), - ); - } + let stored_properties = >::get((collection.id, token_id)); - >::set((collection.id, token_id), stored_properties); - - Ok(()) + >::modify_token_properties( + collection, + sender, + token_id, + properties_updates, + is_token_create, + stored_properties, + is_token_owner, + |properties| >::set((collection.id, token_id), properties), + ) } pub fn set_token_properties( @@ -1462,43 +1403,31 @@ pub fn set_allowance_for_all( collection: &RefungibleHandle, owner: &T::CrossAccountId, - operator: &T::CrossAccountId, + spender: &T::CrossAccountId, approve: bool, ) -> DispatchResult { - if collection.permissions.access() == AccessMode::AllowList { - collection.check_allowlist(owner)?; - collection.check_allowlist(operator)?; - } - - >::ensure_correct_receiver(operator)?; - - // ========= - - >::insert((collection.id, owner, operator), approve); - >::deposit_log( + >::set_allowance_for_all( + collection, + owner, + spender, + approve, + || >::insert((collection.id, owner, spender), approve), ERC721Events::ApprovalForAll { owner: *owner.as_eth(), - operator: *operator.as_eth(), + operator: *spender.as_eth(), approved: approve, } .to_log(collection_id_to_address(collection.id)), - ); - >::deposit_event(CommonEvent::ApprovedForAll( - collection.id, - owner.clone(), - operator.clone(), - approve, - )); - Ok(()) + ) } /// Tells whether the given `owner` approves the `operator`. pub fn allowance_for_all( collection: &RefungibleHandle, owner: &T::CrossAccountId, - operator: &T::CrossAccountId, + spender: &T::CrossAccountId, ) -> bool { - >::get((collection.id, owner, operator)) + >::get((collection.id, owner, spender)) } pub fn repair_item(collection: &RefungibleHandle, token: TokenId) -> DispatchResult { --- a/pallets/refungible/src/stubs/UniqueRefungible.sol +++ b/pallets/refungible/src/stubs/UniqueRefungible.sol @@ -938,7 +938,7 @@ // /// @param tokens array of pairs of token ID and token URI for minted tokens // /// @dev EVM selector for this function is: 0x36543006, // /// or in textual repr: mintBulkWithTokenURI(address,(uint256,string)[]) - // function mintBulkWithTokenURI(address to, Tuple14[] memory tokens) public returns (bool) { + // function mintBulkWithTokenURI(address to, TokenUri[] memory tokens) public returns (bool) { // require(false, stub_error); // to; // tokens; @@ -982,10 +982,12 @@ } } -/// @dev anonymous struct -struct Tuple14 { - uint256 field_0; - string field_1; +/// Data for creation token with uri. +struct TokenUri { + /// Id of new token. + uint256 id; + /// Uri of new token. + string uri; } /// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension --- a/pallets/refungible/src/stubs/UniqueRefungibleToken.sol +++ b/pallets/refungible/src/stubs/UniqueRefungibleToken.sol @@ -36,8 +36,22 @@ } } -/// @dev the ERC-165 identifier for this interface is 0xe17a7d2b +/// @dev the ERC-165 identifier for this interface is 0x01d536fc contract ERC20UniqueExtensions is Dummy, ERC165 { + /// @dev Function to check the amount of tokens that an owner allowed to a spender. + /// @param owner crossAddress The address which owns the funds. + /// @param spender crossAddress The address which will spend the funds. + /// @return A uint256 specifying the amount of tokens still available for the spender. + /// @dev EVM selector for this function is: 0xe0af4bd7, + /// or in textual repr: allowanceCross((address,uint256),(address,uint256)) + function allowanceCross(CrossAddress memory owner, CrossAddress memory spender) public view returns (uint256) { + require(false, stub_error); + owner; + spender; + dummy; + return 0; + } + // /// @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. --- a/runtime/common/ethereum/sponsoring/refungible.rs +++ b/runtime/common/ethereum/sponsoring/refungible.rs @@ -341,7 +341,9 @@ ERC165Call(_, _) => None, // Not sponsored - BurnFrom { .. } | BurnFromCross { .. } | Repartition { .. } => None, + AllowanceCross { .. } | BurnFrom { .. } | BurnFromCross { .. } | Repartition { .. } => { + None + } TransferCross { .. } | TransferFromCross { .. } => { let RefungibleTokenHandle(handle, token_id) = token; --- a/tests/src/eth/abi/fungible.json +++ b/tests/src/eth/abi/fungible.json @@ -101,6 +101,32 @@ { "internalType": "uint256", "name": "sub", "type": "uint256" } ], "internalType": "struct CrossAddress", + "name": "owner", + "type": "tuple" + }, + { + "components": [ + { "internalType": "address", "name": "eth", "type": "address" }, + { "internalType": "uint256", "name": "sub", "type": "uint256" } + ], + "internalType": "struct CrossAddress", + "name": "spender", + "type": "tuple" + } + ], + "name": "allowanceCross", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "eth", "type": "address" }, + { "internalType": "uint256", "name": "sub", "type": "uint256" } + ], + "internalType": "struct CrossAddress", "name": "user", "type": "tuple" } @@ -408,10 +434,10 @@ "inputs": [ { "components": [ - { "internalType": "address", "name": "field_0", "type": "address" }, - { "internalType": "uint256", "name": "field_1", "type": "uint256" } + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } ], - "internalType": "struct Tuple9[]", + "internalType": "struct AmountForAddress[]", "name": "amounts", "type": "tuple[]" } --- a/tests/src/eth/abi/reFungibleToken.json +++ b/tests/src/eth/abi/reFungibleToken.json @@ -61,6 +61,32 @@ }, { "inputs": [ + { + "components": [ + { "internalType": "address", "name": "eth", "type": "address" }, + { "internalType": "uint256", "name": "sub", "type": "uint256" } + ], + "internalType": "struct CrossAddress", + "name": "owner", + "type": "tuple" + }, + { + "components": [ + { "internalType": "address", "name": "eth", "type": "address" }, + { "internalType": "uint256", "name": "sub", "type": "uint256" } + ], + "internalType": "struct CrossAddress", + "name": "spender", + "type": "tuple" + } + ], + "name": "allowanceCross", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], --- a/tests/src/eth/api/UniqueFungible.sol +++ b/tests/src/eth/api/UniqueFungible.sol @@ -353,8 +353,16 @@ bytes value; } -/// @dev the ERC-165 identifier for this interface is 0x65789571 +/// @dev the ERC-165 identifier for this interface is 0x85d7dea6 interface ERC20UniqueExtensions is Dummy, ERC165 { + /// @dev Function to check the amount of tokens that an owner allowed to a spender. + /// @param owner crossAddress The address which owns the funds. + /// @param spender crossAddress The address which will spend the funds. + /// @return A uint256 specifying the amount of tokens still available for the spender. + /// @dev EVM selector for this function is: 0xe0af4bd7, + /// or in textual repr: allowanceCross((address,uint256),(address,uint256)) + function allowanceCross(CrossAddress memory owner, CrossAddress memory spender) external view returns (uint256); + /// @notice A description for the collection. /// @dev EVM selector for this function is: 0x7284e416, /// or in textual repr: description() @@ -390,7 +398,7 @@ /// @param amounts array of pairs of account address and amount /// @dev EVM selector for this function is: 0x1acf2d55, /// or in textual repr: mintBulk((address,uint256)[]) - function mintBulk(Tuple9[] memory amounts) external returns (bool); + function mintBulk(AmountForAddress[] memory amounts) external returns (bool); /// @dev EVM selector for this function is: 0x2ada85ff, /// or in textual repr: transferCross((address,uint256),uint256) @@ -410,10 +418,9 @@ function collectionHelperAddress() external view returns (address); } -/// @dev anonymous struct -struct Tuple9 { - address field_0; - uint256 field_1; +struct AmountForAddress { + address to; + uint256 amount; } /// @dev the ERC-165 identifier for this interface is 0x40c10f19 --- a/tests/src/eth/api/UniqueNFT.sol +++ b/tests/src/eth/api/UniqueNFT.sol @@ -644,7 +644,7 @@ // /// @param tokens array of pairs of token ID and token URI for minted tokens // /// @dev EVM selector for this function is: 0x36543006, // /// or in textual repr: mintBulkWithTokenURI(address,(uint256,string)[]) - // function mintBulkWithTokenURI(address to, Tuple13[] memory tokens) external returns (bool); + // function mintBulkWithTokenURI(address to, TokenUri[] memory tokens) external returns (bool); /// @notice Function to mint a token. /// @param to The new owner crossAccountId @@ -660,10 +660,12 @@ function collectionHelperAddress() external view returns (address); } -/// @dev anonymous struct -struct Tuple13 { - uint256 field_0; - string field_1; +/// Data for creation token with uri. +struct TokenUri { + /// Id of new token. + uint256 id; + /// Uri of new token. + string uri; } /// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension --- a/tests/src/eth/api/UniqueRefungible.sol +++ b/tests/src/eth/api/UniqueRefungible.sol @@ -638,7 +638,7 @@ // /// @param tokens array of pairs of token ID and token URI for minted tokens // /// @dev EVM selector for this function is: 0x36543006, // /// or in textual repr: mintBulkWithTokenURI(address,(uint256,string)[]) - // function mintBulkWithTokenURI(address to, Tuple12[] memory tokens) external returns (bool); + // function mintBulkWithTokenURI(address to, TokenUri[] memory tokens) external returns (bool); /// @notice Function to mint a token. /// @param to The new owner crossAccountId @@ -661,10 +661,12 @@ function collectionHelperAddress() external view returns (address); } -/// @dev anonymous struct -struct Tuple12 { - uint256 field_0; - string field_1; +/// Data for creation token with uri. +struct TokenUri { + /// Id of new token. + uint256 id; + /// Uri of new token. + string uri; } /// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension --- a/tests/src/eth/api/UniqueRefungibleToken.sol +++ b/tests/src/eth/api/UniqueRefungibleToken.sol @@ -23,8 +23,16 @@ function parentTokenId() external view returns (uint256); } -/// @dev the ERC-165 identifier for this interface is 0xe17a7d2b +/// @dev the ERC-165 identifier for this interface is 0x01d536fc interface ERC20UniqueExtensions is Dummy, ERC165 { + /// @dev Function to check the amount of tokens that an owner allowed to a spender. + /// @param owner crossAddress The address which owns the funds. + /// @param spender crossAddress The address which will spend the funds. + /// @return A uint256 specifying the amount of tokens still available for the spender. + /// @dev EVM selector for this function is: 0xe0af4bd7, + /// or in textual repr: allowanceCross((address,uint256),(address,uint256)) + function allowanceCross(CrossAddress memory owner, CrossAddress memory spender) external view returns (uint256); + // /// @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. --- a/tests/src/eth/fungible.test.ts +++ b/tests/src/eth/fungible.test.ts @@ -134,6 +134,12 @@ const allowance = await contract.methods.allowance(owner, spender).call(); expect(+allowance).to.equal(100); } + { + const ownerCross = helper.ethCrossAccount.fromAddress(owner); + const spenderCross = helper.ethCrossAccount.fromAddress(spender); + const allowance = await contract.methods.allowanceCross(ownerCross, spenderCross).call(); + expect(+allowance).to.equal(100); + } }); itEth('Can perform approveCross()', async ({helper}) => { --- a/tests/src/eth/proxy/UniqueNFTProxy.sol +++ b/tests/src/eth/proxy/UniqueNFTProxy.sol @@ -168,7 +168,7 @@ return proxied.mintBulk(to, tokenIds); } - function mintBulkWithTokenURI(address to, Tuple6[] memory tokens) + function mintBulkWithTokenURI(address to, TokenUri[] memory tokens) external override returns (bool) --- a/tests/src/eth/reFungible.test.ts +++ b/tests/src/eth/reFungible.test.ts @@ -219,8 +219,16 @@ await rftToken.methods.approve(operator, 15n).send({from: owner}); await contract.methods.setApprovalForAll(operator, true).send({from: owner}); await rftToken.methods.burnFrom(owner, 10n).send({from: operator}); + } + { const allowance = await rftToken.methods.allowance(owner, operator).call(); - expect(allowance).to.be.equal('5'); + expect(+allowance).to.be.equal(5); + } + { + const ownerCross = helper.ethCrossAccount.fromAddress(owner); + const operatorCross = helper.ethCrossAccount.fromAddress(operator); + const allowance = await rftToken.methods.allowanceCross(ownerCross, operatorCross).call(); + expect(+allowance).to.equal(5); } });