--- /dev/null +++ b/pallets/nonfungible/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "pallet-nonfungible" +version = "0.1.0" +edition = "2018" + +[dependencies.codec] +default-features = false +features = ['derive'] +package = 'parity-scale-codec' +version = '2.0.0' + +[dependencies] +frame-support = { default-features = false, version = '4.0.0-dev', git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.10' } +frame-system = { default-features = false, version = '4.0.0-dev', git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.10' } +sp-runtime = { default-features = false, version = '4.0.0-dev', git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.10' } +sp-std = { default-features = false, version = '4.0.0-dev', git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.10' } +sp-core = { default-features = false, version = '4.0.0-dev', git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.10' } +pallet-common = { default-features = false, path = '../common' } +nft-data-structs = { default-features = false, path = '../../primitives/nft' } +evm-coder = { default-features = false, path = '../../crates/evm-coder' } +pallet-evm-coder-substrate = { default-features = false, path = '../evm-coder-substrate' } +ethereum = { default-features = false, version = "0.9.0" } + +[features] +default = ["std"] +std = [ + "frame-support/std", + "frame-system/std", + "sp-runtime/std", + "sp-std/std", + "nft-data-structs/std", + "pallet-common/std", + "evm-coder/std", + "ethereum/std", + "pallet-evm-coder-substrate/std", +] +runtime-benchmarks = [] --- /dev/null +++ b/pallets/nonfungible/src/benchmarking.rs @@ -0,0 +1 @@ +#![cfg(feature = "runtime-benchmarking")] --- /dev/null +++ b/pallets/nonfungible/src/common.rs @@ -0,0 +1,228 @@ +use core::marker::PhantomData; + +use frame_support::{dispatch::DispatchResultWithPostInfo, ensure, fail, weights::Weight}; +use nft_data_structs::TokenId; +use pallet_common::{ + CommonCollectionOperations, CommonWeightInfo, account::CrossAccountId, with_weight, +}; +use sp_runtime::DispatchError; +use sp_std::vec::Vec; + +use crate::{ + AccountBalance, Allowance, Config, CreateItemData, DataKind, Error, NonfungibleHandle, Owned, + Owner, Pallet, SelfWeightOf, TokenData, weights::WeightInfo, +}; + +pub struct CommonWeights(PhantomData); +impl CommonWeightInfo for CommonWeights { + fn create_item() -> Weight { + >::create_item() + } + + fn create_multiple_items(amount: u32) -> Weight { + >::create_multiple_items(amount) + } + + fn burn_item() -> Weight { + >::burn_item() + } + + fn transfer() -> Weight { + >::transfer() + } + + fn approve() -> Weight { + >::approve() + } + + fn transfer_from() -> Weight { + >::transfer_from() + } + + fn set_variable_metadata(_bytes: u32) -> Weight { + >::set_variable_metadata() + } +} + +fn map_create_data( + data: nft_data_structs::CreateItemData, + to: &T::CrossAccountId, +) -> Result, DispatchError> { + match data { + nft_data_structs::CreateItemData::NFT(data) => Ok(CreateItemData { + const_data: data.const_data, + variable_data: data.variable_data, + owner: to.clone(), + }), + _ => fail!(>::NotNonfungibleDataUsedToMintFungibleCollectionToken), + } +} + +impl CommonCollectionOperations for NonfungibleHandle { + fn create_item( + &self, + sender: T::CrossAccountId, + to: T::CrossAccountId, + data: nft_data_structs::CreateItemData, + ) -> DispatchResultWithPostInfo { + with_weight( + >::create_item(self, &sender, map_create_data(data, &to)?), + >::create_item(), + ) + } + + fn create_multiple_items( + &self, + sender: T::CrossAccountId, + to: T::CrossAccountId, + data: Vec, + ) -> DispatchResultWithPostInfo { + let data = data + .into_iter() + .map(|d| map_create_data::(d, &to)) + .collect::, DispatchError>>()?; + + let amount = data.len(); + with_weight( + >::create_multiple_items(self, &sender, data), + >::create_multiple_items(amount as u32), + ) + } + + fn burn_item( + &self, + sender: T::CrossAccountId, + token: TokenId, + amount: u128, + ) -> DispatchResultWithPostInfo { + ensure!(amount <= 1, >::NonfungibleItemsHaveNoAmount); + if amount == 1 { + with_weight( + >::burn(&self, &sender, token), + >::burn_item(), + ) + } else { + Ok(().into()) + } + } + + fn transfer( + &self, + from: T::CrossAccountId, + to: T::CrossAccountId, + token: TokenId, + amount: u128, + ) -> DispatchResultWithPostInfo { + ensure!(amount <= 1, >::NonfungibleItemsHaveNoAmount); + if amount == 1 { + with_weight( + >::transfer(&self, &from, &to, token), + >::transfer(), + ) + } else { + Ok(().into()) + } + } + + fn approve( + &self, + sender: T::CrossAccountId, + spender: T::CrossAccountId, + token: TokenId, + amount: u128, + ) -> DispatchResultWithPostInfo { + ensure!(amount <= 1, >::NonfungibleItemsHaveNoAmount); + + with_weight( + if amount == 1 { + >::set_allowance(&self, &sender, token, Some(&spender)) + } else { + >::set_allowance(&self, &sender, token, None) + }, + >::approve(), + ) + } + + fn transfer_from( + &self, + sender: T::CrossAccountId, + from: T::CrossAccountId, + to: T::CrossAccountId, + token: TokenId, + amount: u128, + ) -> DispatchResultWithPostInfo { + ensure!(amount <= 1, >::NonfungibleItemsHaveNoAmount); + + if amount == 1 { + with_weight( + >::transfer_from(&self, &sender, &from, &to, token), + >::transfer_from(), + ) + } else { + Ok(().into()) + } + } + + fn set_variable_metadata( + &self, + sender: T::CrossAccountId, + token: TokenId, + data: Vec, + ) -> DispatchResultWithPostInfo { + with_weight( + >::set_variable_metadata(&self, &sender, token, data), + >::set_variable_metadata(), + ) + } + + fn account_tokens(&self, account: T::CrossAccountId) -> Vec { + >::iter_prefix((self.id, account.as_sub())) + .map(|(id, _)| id) + .collect() + } + + fn token_exists(&self, token: TokenId) -> bool { + >::token_exists(self, token) + } + + fn token_owner(&self, token: TokenId) -> T::CrossAccountId { + >::get((self.id, token)) + } + fn const_metadata(&self, token: TokenId) -> Vec { + >::get((self.id, token, DataKind::Constant)) + } + fn variable_metadata(&self, token: TokenId) -> Vec { + >::get((self.id, token, DataKind::Variable)) + } + + fn collection_tokens(&self) -> u32 { + >::total_supply(self) + } + + fn account_balance(&self, account: T::CrossAccountId) -> u32 { + >::get((self.id, account.as_sub())) + } + + fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128 { + if >::get((self.id, token)) == account { + 1 + } else { + 0 + } + } + + fn allowance( + &self, + sender: T::CrossAccountId, + spender: T::CrossAccountId, + token: TokenId, + ) -> u128 { + if >::get((self.id, token)) != sender { + 0 + } else if >::get((self.id, token)) == Some(spender) { + 1 + } else { + 0 + } + } +} --- /dev/null +++ b/pallets/nonfungible/src/erc.rs @@ -0,0 +1,396 @@ +use core::{ + char::{REPLACEMENT_CHARACTER, decode_utf16}, + convert::TryInto, +}; +use evm_coder::{ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*}; +use frame_support::BoundedVec; +use nft_data_structs::TokenId; +use pallet_evm_coder_substrate::dispatch_to_evm; +use sp_core::{H160, U256}; +use sp_std::{vec::Vec, vec}; +use pallet_common::{account::CrossAccountId, erc::CommonEvmHandler}; +use pallet_evm_coder_substrate::call_internal; +use pallet_common::erc::PrecompileOutput; + +use crate::{ + AccountBalance, Config, CreateItemData, DataKind, NonfungibleHandle, Owner, Pallet, TokenData, + TokensMinted, +}; + +#[derive(ToLog)] +pub enum ERC721Events { + Transfer { + #[indexed] + from: address, + #[indexed] + to: address, + #[indexed] + token_id: uint256, + }, + Approval { + #[indexed] + owner: address, + #[indexed] + approved: address, + #[indexed] + token_id: uint256, + }, + #[allow(dead_code)] + ApprovalForAll { + #[indexed] + owner: address, + #[indexed] + operator: address, + approved: bool, + }, +} + +#[derive(ToLog)] +pub enum ERC721MintableEvents { + #[allow(dead_code)] + MintingFinished {}, +} + +#[solidity_interface(name = "ERC721Metadata")] +impl NonfungibleHandle { + fn name(&self) -> Result { + Ok(decode_utf16(self.name.iter().copied()) + .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER)) + .collect::()) + } + fn symbol(&self) -> Result { + Ok(string::from_utf8_lossy(&self.token_prefix).into()) + } + + #[solidity(rename_selector = "tokenURI")] + fn token_uri(&self, token_id: uint256) -> Result { + let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?; + Ok(string::from_utf8_lossy(&>::get(( + self.id, + token_id, + DataKind::Constant, + ))) + .into()) + } +} + +#[solidity_interface(name = "ERC721Enumerable")] +impl NonfungibleHandle { + fn token_by_index(&self, index: uint256) -> Result { + Ok(index) + } + + fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result { + // TODO: Not implemetable + Err("not implemented".into()) + } + + fn total_supply(&self) -> Result { + Ok(>::total_supply(self).into()) + } +} + +#[solidity_interface(name = "ERC721", events(ERC721Events))] +impl NonfungibleHandle { + fn balance_of(&self, owner: address) -> Result { + let owner = T::CrossAccountId::from_eth(owner); + let balance = >::get((self.id, owner.as_sub())); + Ok(balance.into()) + } + fn owner_of(&self, token_id: uint256) -> Result
{ + let token: TokenId = token_id.try_into()?; + Ok(*>::get((self.id, token)).as_eth()) + } + fn safe_transfer_from_with_data( + &mut self, + _from: address, + _to: address, + _token_id: uint256, + _data: bytes, + _value: value, + ) -> Result { + // TODO: Not implemetable + Err("not implemented".into()) + } + fn safe_transfer_from( + &mut self, + _from: address, + _to: address, + _token_id: uint256, + _value: value, + ) -> Result { + // TODO: Not implemetable + Err("not implemented".into()) + } + + fn transfer_from( + &mut self, + caller: caller, + from: address, + to: address, + token_id: uint256, + _value: value, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let from = T::CrossAccountId::from_eth(from); + let to = T::CrossAccountId::from_eth(to); + let token = token_id.try_into()?; + + >::transfer_from(self, &caller, &from, &to, token) + .map_err(dispatch_to_evm::)?; + Ok(()) + } + + fn approve( + &mut self, + caller: caller, + approved: address, + token_id: uint256, + _value: value, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let approved = T::CrossAccountId::from_eth(approved); + let token = token_id.try_into()?; + + >::set_allowance(self, &caller, token, Some(&approved)) + .map_err(dispatch_to_evm::)?; + Ok(()) + } + + fn set_approval_for_all( + &mut self, + _caller: caller, + _operator: address, + _approved: bool, + ) -> Result { + // TODO: Not implemetable + Err("not implemented".into()) + } + + fn get_approved(&self, _token_id: uint256) -> Result
{ + // TODO: Not implemetable + Err("not implemented".into()) + } + + fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result
{ + // TODO: Not implemetable + Err("not implemented".into()) + } +} + +#[solidity_interface(name = "ERC721Burnable")] +impl NonfungibleHandle { + fn burn(&mut self, caller: caller, token_id: uint256) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let token = token_id.try_into()?; + + >::burn(self, &caller, token).map_err(dispatch_to_evm::)?; + Ok(()) + } +} + +#[solidity_interface(name = "ERC721Mintable", events(ERC721MintableEvents))] +impl NonfungibleHandle { + fn minting_finished(&self) -> Result { + Ok(false) + } + + fn mint(&mut self, caller: caller, to: address, token_id: uint256) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let to = T::CrossAccountId::from_eth(to); + let token_id: u32 = token_id.try_into()?; + if >::get(self.id) + .checked_add(1) + .ok_or("item id overflow")? + != token_id + { + return Err("item id should be next".into()); + } + + >::create_item( + self, + &caller, + CreateItemData { + const_data: BoundedVec::default(), + variable_data: BoundedVec::default(), + owner: to, + }, + ) + .map_err(dispatch_to_evm::)?; + + Ok(true) + } + + #[solidity(rename_selector = "mintWithTokenURI")] + fn mint_with_token_uri( + &mut self, + caller: caller, + to: address, + token_id: uint256, + token_uri: string, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let to = T::CrossAccountId::from_eth(to); + let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?; + if >::get(self.id) + .checked_add(1) + .ok_or("item id overflow")? + != token_id + { + return Err("item id should be next".into()); + } + + >::create_item( + self, + &caller, + CreateItemData { + const_data: Vec::::from(token_uri) + .try_into() + .map_err(|_| "token uri is too long")?, + variable_data: BoundedVec::default(), + owner: to, + }, + ) + .map_err(dispatch_to_evm::)?; + Ok(true) + } + + fn finish_minting(&mut self, _caller: caller) -> Result { + Err("not implementable".into()) + } +} + +#[solidity_interface(name = "ERC721UniqueExtensions")] +impl NonfungibleHandle { + #[solidity(rename_selector = "transfer")] + fn transfer_nft( + &mut self, + caller: caller, + to: address, + token_id: uint256, + _value: value, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let to = T::CrossAccountId::from_eth(to); + let token = token_id.try_into()?; + + >::transfer(self, &caller, &to, token).map_err(dispatch_to_evm::)?; + Ok(()) + } + + fn next_token_id(&self) -> Result { + Ok(>::get(self.id) + .checked_add(1) + .ok_or("item id overflow")? + .into()) + } + + fn set_variable_metadata( + &mut self, + caller: caller, + token_id: uint256, + data: bytes, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let token = token_id.try_into()?; + + >::set_variable_metadata(self, &caller, token, data) + .map_err(dispatch_to_evm::)?; + Ok(()) + } + + fn get_variable_metadata(&self, token_id: uint256) -> Result { + let token: TokenId = token_id.try_into()?; + + Ok(>::get((self.id, token, DataKind::Variable))) + } + + fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let to = T::CrossAccountId::from_eth(to); + let mut expected_index = >::get(self.id) + .checked_add(1) + .ok_or("item id overflow")?; + + let total_tokens = token_ids.len(); + for id in token_ids.into_iter() { + let id: u32 = id.try_into().map_err(|_| "token id overflow")?; + if id != expected_index { + return Err("item id should be next".into()); + } + expected_index = expected_index.checked_add(1).ok_or("item id overflow")?; + } + let data = (0..total_tokens) + .map(|_| CreateItemData { + const_data: BoundedVec::default(), + variable_data: BoundedVec::default(), + owner: to.clone(), + }) + .collect(); + + >::create_multiple_items(self, &caller, data).map_err(dispatch_to_evm::)?; + Ok(true) + } + + #[solidity(rename_selector = "mintBulkWithTokenURI")] + fn mint_bulk_with_token_uri( + &mut self, + caller: caller, + to: address, + tokens: Vec<(uint256, string)>, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let to = T::CrossAccountId::from_eth(to); + let mut expected_index = >::get(self.id) + .checked_add(1) + .ok_or("item id overflow")?; + + let mut data = Vec::with_capacity(tokens.len()); + for (id, token_uri) in tokens { + let id: u32 = id.try_into().map_err(|_| "token id overflow")?; + if id != expected_index { + panic!("item id should be next ({}) but got {}", expected_index, id); + } + expected_index = expected_index.checked_add(1).ok_or("item id overflow")?; + + data.push(CreateItemData { + const_data: Vec::::from(token_uri) + .try_into() + .map_err(|_| "token uri is too long")?, + variable_data: vec![].try_into().unwrap(), + owner: to.clone(), + }); + } + + >::create_multiple_items(self, &caller, data).map_err(dispatch_to_evm::)?; + Ok(true) + } +} + +#[solidity_interface( + name = "UniqueNFT", + is( + ERC721, + ERC721Metadata, + ERC721Enumerable, + ERC721UniqueExtensions, + ERC721Mintable, + ERC721Burnable, + ) +)] +impl NonfungibleHandle {} + +// Not a tests, but code generators +generate_stubgen!(gen_impl, UniqueNFTCall, true); +generate_stubgen!(gen_iface, UniqueNFTCall, false); + +pub const CODE: &[u8] = include_bytes!("./stubs/UniqueNFT.raw"); + +impl CommonEvmHandler for NonfungibleHandle { + const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw"); + + fn call(mut self, source: &H160, input: &[u8], value: U256) -> Option { + let result = call_internal::(*source, &mut self, value, input); + self.0.recorder.evm_to_precompile_output(result) + } +} --- /dev/null +++ b/pallets/nonfungible/src/lib.rs @@ -0,0 +1,537 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use erc::ERC721Events; +use frame_support::{BoundedVec, ensure}; +use nft_data_structs::{ + AccessMode, CUSTOM_DATA_LIMIT, Collection, CollectionId, CustomDataLimit, TokenId, +}; +use pallet_common::{ + Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, account::CrossAccountId, +}; +use sp_core::H160; +use sp_runtime::{ArithmeticError, DispatchError, DispatchResult}; +use sp_std::{vec::Vec, vec}; +use core::ops::Deref; +use sp_std::collections::btree_map::BTreeMap; + +pub use pallet::*; +pub mod benchmarking; +pub mod common; +pub mod erc; +pub mod weights; + +pub struct CreateItemData { + pub const_data: BoundedVec, + pub variable_data: BoundedVec, + pub owner: T::CrossAccountId, +} +pub(crate) type SelfWeightOf = ::WeightInfo; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::{Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key}; + use sp_std::vec::Vec; + use nft_data_structs::{CollectionId, TokenId}; + use super::weights::WeightInfo; + + #[pallet::error] + pub enum Error { + /// Not Nonfungible item data used to mint in Nonfungible collection. + NotNonfungibleDataUsedToMintFungibleCollectionToken, + /// Used amount > 1 with NFT + NonfungibleItemsHaveNoAmount, + } + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_common::Config { + type WeightInfo: WeightInfo; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::storage] + pub(super) type TokensMinted = + StorageMap; + #[pallet::storage] + pub(super) type TokensBurnt = + StorageMap; + + #[derive(Encode, Decode)] + pub enum DataKind { + Constant, + Variable, + } + + #[pallet::storage] + pub(super) type TokenData = StorageNMap< + Key = ( + Key, + Key, + Key, + ), + Value = Vec, + QueryKind = ValueQuery, + >; + + #[pallet::storage] + pub(super) type Owner = StorageNMap< + Key = (Key, Key), + Value = T::CrossAccountId, + QueryKind = ValueQuery, + >; + /// Used to enumerate tokens owned by account + #[pallet::storage] + pub(super) type Owned = StorageNMap< + Key = ( + Key, + Key, + Key, + ), + Value = bool, + QueryKind = ValueQuery, + >; + + #[pallet::storage] + pub(super) type AccountBalance = StorageNMap< + Key = ( + Key, + Key, + ), + Value = u32, + QueryKind = ValueQuery, + >; + + #[pallet::storage] + pub(super) type Allowance = StorageNMap< + Key = (Key, Key), + Value = T::CrossAccountId, + QueryKind = OptionQuery, + >; +} + +pub struct NonfungibleHandle(pallet_common::CollectionHandle); +impl NonfungibleHandle { + pub fn cast(inner: pallet_common::CollectionHandle) -> Self { + Self(inner) + } + pub fn into_inner(self) -> pallet_common::CollectionHandle { + self.0 + } +} +impl Deref for NonfungibleHandle { + type Target = pallet_common::CollectionHandle; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Pallet { + pub fn total_supply(collection: &NonfungibleHandle) -> u32 { + >::get(collection.id) - >::get(collection.id) + } + pub fn token_exists(collection: &NonfungibleHandle, token: TokenId) -> bool { + >::contains_key((collection.id, token)) + } + pub fn ensure_owner( + collection: &NonfungibleHandle, + token: TokenId, + sender: &T::CrossAccountId, + ) -> DispatchResult { + ensure!( + &>::get((collection.id, token)) == sender, + >::NoPermission + ); + Ok(()) + } + pub fn item_owner( + collection: &NonfungibleHandle, + token: TokenId, + ) -> Result { + let owner = >::get((collection.id, token)); + ensure!( + owner != T::CrossAccountId::default(), + >::TokenNotFound + ); + Ok(owner) + } +} + +// unchecked calls skips any permission checks +impl Pallet { + pub fn init_collection(data: Collection) -> Result { + PalletCommon::init_collection(data) + } + pub fn destroy_collection( + collection: NonfungibleHandle, + sender: &T::CrossAccountId, + ) -> DispatchResult { + let id = collection.id; + + // ========= + + PalletCommon::destroy_collection(collection.0, sender)?; + + >::remove_prefix((id,), None); + >::remove_prefix((id,), None); + >::remove(id); + >::remove(id); + >::remove_prefix((id,), None); + >::remove_prefix((id,), None); + >::remove_prefix((id,), None); + Ok(()) + } + + pub fn burn( + collection: &NonfungibleHandle, + sender: &T::CrossAccountId, + token: TokenId, + ) -> DispatchResult { + let token_owner = >::item_owner(collection, token)?; + ensure!( + &token_owner == sender + || (collection.limits.owner_can_transfer + && collection.is_owner_or_admin(sender)?), + >::NoPermission + ); + + if collection.access == AccessMode::WhiteList { + collection.check_whitelist(sender)?; + } + + let burnt = >::get(collection.id) + .checked_add(1) + .ok_or(ArithmeticError::Overflow)?; + + // ========= + + >::remove((collection.id, token)); + >::remove((collection.id, token_owner.as_sub(), token)); + >::insert(collection.id, burnt); + >::remove_prefix((collection.id, token), None); + >::remove((collection.id, token)); + + collection.log_infallible(ERC721Events::Transfer { + from: *token_owner.as_eth(), + to: H160::default(), + token_id: token.into(), + }); + >::deposit_event(CommonEvent::ItemDestroyed( + collection.id, + token, + token_owner, + 1, + )); + return Ok(()); + } + + pub fn transfer( + collection: &NonfungibleHandle, + from: &T::CrossAccountId, + to: &T::CrossAccountId, + token: TokenId, + ) -> DispatchResult { + ensure!( + collection.transfers_enabled, + >::TransferNotAllowed + ); + + let token_owner = >::item_owner(collection, token)?; + ensure!( + &token_owner == from + || (collection.limits.owner_can_transfer && collection.is_owner_or_admin(from)?), + >::NoPermission + ); + + if collection.access == AccessMode::WhiteList { + collection.check_whitelist(from)?; + collection.check_whitelist(to)?; + } + >::ensure_correct_receiver(to)?; + + let balance_from = >::get((collection.id, from.as_sub())) + .checked_sub(1) + .ok_or(>::TokenValueTooLow)?; + let balance_to = if from != to { + let balance_to = >::get((collection.id, to.as_sub())) + .checked_add(1) + .ok_or(ArithmeticError::Overflow)?; + + ensure!( + balance_to < collection.limits.account_token_ownership_limit(), + >::AccountTokenLimitExceeded, + ); + + Some(balance_to) + } else { + None + }; + + collection.consume_sstores(4)?; + collection.consume_log(3, 0)?; + + // ========= + + if let Some(balance_to) = balance_to { + // from != to + if balance_from == 0 { + >::remove((collection.id, from.as_sub())); + } else { + >::insert((collection.id, from.as_sub()), balance_from); + } + >::insert((collection.id, to.as_sub()), balance_to); + >::remove((collection.id, from.as_sub(), token)); + >::insert((collection.id, to.as_sub(), token), true); + } + Self::set_allowance_unchecked(collection, from, token, None); + >::insert((collection.id, token), &to); + + collection.log_infallible(ERC721Events::Transfer { + from: *from.as_eth(), + to: *to.as_eth(), + token_id: token.into(), + }); + >::deposit_event(CommonEvent::Transfer( + collection.id, + token, + from.clone(), + to.clone(), + 1, + )); + Ok(()) + } + + pub fn create_multiple_items( + collection: &NonfungibleHandle, + sender: &T::CrossAccountId, + data: Vec>, + ) -> DispatchResult { + let unrestricted_minting = collection.is_owner_or_admin(sender)?; + if !unrestricted_minting { + ensure!( + collection.mint_mode, + >::PublicMintingNotAllowed + ); + collection.check_whitelist(sender)?; + + for item in data.iter() { + collection.check_whitelist(&item.owner)?; + } + } + + for data in data.iter() { + >::ensure_correct_receiver(&data.owner)?; + if !data.const_data.is_empty() { + collection.consume_sstore()?; + } + if !data.variable_data.is_empty() { + collection.consume_sstore()?; + } + collection.consume_sstore()?; + collection.consume_log(3, 0)?; + } + + let first_token = >::get(collection.id); + let tokens_minted = first_token + .checked_add(data.len() as u32) + .ok_or(ArithmeticError::Overflow)?; + ensure!( + tokens_minted < collection.limits.token_limit, + >::CollectionTokenLimitExceeded + ); + collection.consume_sstore()?; + + let mut balances = BTreeMap::new(); + for data in &data { + let balance = balances + .entry(data.owner.as_sub()) + .or_insert_with(|| >::get((collection.id, data.owner.as_sub()))); + *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?; + + ensure!( + *balance <= collection.limits.account_token_ownership_limit(), + >::AccountTokenLimitExceeded, + ); + } + collection.consume_sstores(balances.len())?; + + // ========= + + >::insert(collection.id, tokens_minted); + for (account, balance) in balances { + >::insert((collection.id, account), balance); + } + for (i, data) in data.into_iter().enumerate() { + let token = first_token + i as u32; + + if !data.const_data.is_empty() { + >::insert((collection.id, token, DataKind::Constant), data.const_data); + } + if !data.variable_data.is_empty() { + >::insert( + (collection.id, token, DataKind::Variable), + data.variable_data, + ); + } + >::insert((collection.id, token), &data.owner); + >::insert((collection.id, data.owner.as_sub(), token), true); + + collection.log_infallible(ERC721Events::Transfer { + from: H160::default(), + to: *data.owner.as_eth(), + token_id: token.into(), + }); + } + Ok(()) + } + + pub fn set_allowance_unchecked( + collection: &NonfungibleHandle, + sender: &T::CrossAccountId, + token: TokenId, + spender: Option<&T::CrossAccountId>, + ) { + if let Some(spender) = spender { + let old_spender = >::get((collection.id, token)); + >::insert((collection.id, token), spender); + // In ERC721 there is only one possible approved user of token, so we set + // approved user to spender + collection.log_infallible(ERC721Events::Approval { + owner: *sender.as_eth(), + approved: *spender.as_eth(), + token_id: token.into(), + }); + // In Unique chain, any token can have any amount of approved users, so we need to + // set allowance of old owner to 0, and allowance of new owner to 1 + if old_spender.as_ref() != Some(spender) { + if let Some(old_owner) = old_spender { + >::deposit_event(CommonEvent::Approved( + collection.id, + token, + sender.clone(), + old_owner.clone(), + 0, + )); + } + >::deposit_event(CommonEvent::Approved( + collection.id, + token, + sender.clone(), + spender.clone(), + 1, + )); + } + } else { + let old_spender = >::take((collection.id, token)); + // In ERC721 there is only one possible approved user of token, so we set + // approved user to zero address + collection.log_infallible(ERC721Events::Approval { + owner: *sender.as_eth(), + approved: H160::default(), + token_id: token.into(), + }); + // In Unique chain, any token can have any amount of approved users, so we need to + // set allowance of old owner to 0 + if let Some(old_spender) = old_spender { + >::deposit_event(CommonEvent::Approved( + collection.id, + token, + sender.clone(), + old_spender.clone(), + 0, + )); + } + } + } + + pub fn set_allowance( + collection: &NonfungibleHandle, + sender: &T::CrossAccountId, + token: TokenId, + spender: Option<&T::CrossAccountId>, + ) -> DispatchResult { + if collection.access == AccessMode::WhiteList { + collection.check_whitelist(&sender)?; + if let Some(spender) = spender { + collection.check_whitelist(&spender)?; + } + } + + if let Some(spender) = spender { + >::ensure_correct_receiver(spender)?; + } + let token_owner = Self::item_owner(collection, token)?; + if &token_owner != sender { + ensure!( + collection.ignores_owned_amount(sender)?, + >::CantApproveMoreThanOwned + ); + } + + // ========= + + Self::set_allowance_unchecked(collection, sender, token, spender); + Ok(()) + } + + pub fn transfer_from( + collection: &NonfungibleHandle, + spender: &T::CrossAccountId, + from: &T::CrossAccountId, + to: &T::CrossAccountId, + token: TokenId, + ) -> DispatchResult { + if spender == from { + return Self::transfer(collection, from, to, token); + } + if collection.access == AccessMode::WhiteList { + // `from`, `to` checked in [`transfer`] + collection.check_whitelist(spender)?; + } + + if >::get((collection.id, token)).as_ref() != Some(spender) { + ensure!( + collection.ignores_allowance(spender)?, + >::TokenValueNotEnough + ); + } + + // ========= + + Self::transfer(collection, &from, to, token)?; + // Allowance is reset in [`transfer`] + Ok(()) + } + + pub fn set_variable_metadata( + collection: &NonfungibleHandle, + sender: &T::CrossAccountId, + token: TokenId, + data: Vec, + ) -> DispatchResult { + ensure!( + data.len() as u32 <= CUSTOM_DATA_LIMIT, + >::TokenVariableDataLimitExceeded + ); + let item_owner = Self::item_owner(collection, token)?; + collection.check_can_update_meta(sender, &item_owner)?; + + collection.consume_sstore()?; + + // ========= + + >::insert((collection.id, token, DataKind::Variable), data); + Ok(()) + } + + /// Delegated to `create_multiple_items` + pub fn create_item( + collection: &NonfungibleHandle, + sender: &T::CrossAccountId, + data: CreateItemData, + ) -> DispatchResult { + Self::create_multiple_items(collection, sender, vec![data]) + } +} --- /dev/null +++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: OTHER +// This code is automatically generated + +pragma solidity >=0.8.0 <0.9.0; + +// Anonymous struct +struct Tuple0 { + uint256 field_0; + string field_1; +} + +// Common stubs holder +contract Dummy { + uint8 dummy; + string stub_error = "this contract is implemented in native"; +} + +// Inline +contract ERC721Events { + event Transfer( + address indexed from, + address indexed to, + uint256 indexed tokenId + ); + event Approval( + address indexed owner, + address indexed approved, + uint256 indexed tokenId + ); + event ApprovalForAll( + address indexed owner, + address indexed operator, + bool approved + ); +} + +// Inline +contract ERC721MintableEvents { + event MintingFinished(); +} + +// Inline +contract InlineNameSymbol is Dummy { + // 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 ""; + } +} + +// Inline +contract InlineTotalSupply is Dummy { + // Selector: totalSupply() 18160ddd + function totalSupply() public view returns (uint256) { + require(false, stub_error); + dummy; + return 0; + } +} + +contract ERC165 is Dummy { + // Selector: supportsInterface(bytes4) 01ffc9a7 + function supportsInterface(uint32 interfaceId) public view returns (bool) { + require(false, stub_error); + interfaceId; + dummy; + return false; + } +} + +contract ERC721 is Dummy, ERC165, ERC721Events { + // Selector: balanceOf(address) 70a08231 + function balanceOf(address owner) public view returns (uint256) { + require(false, stub_error); + owner; + dummy; + return 0; + } + + // Selector: ownerOf(uint256) 6352211e + function ownerOf(uint256 tokenId) public view returns (address) { + require(false, stub_error); + tokenId; + dummy; + return 0x0000000000000000000000000000000000000000; + } + + // Selector: safeTransferFromWithData(address,address,uint256,bytes) 60a11672 + function safeTransferFromWithData( + address from, + address to, + uint256 tokenId, + bytes memory data + ) public { + require(false, stub_error); + from; + to; + tokenId; + data; + dummy = 0; + } + + // Selector: safeTransferFrom(address,address,uint256) 42842e0e + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public { + require(false, stub_error); + from; + to; + tokenId; + dummy = 0; + } + + // Selector: transferFrom(address,address,uint256) 23b872dd + function transferFrom( + address from, + address to, + uint256 tokenId + ) public { + require(false, stub_error); + from; + to; + tokenId; + dummy = 0; + } + + // Selector: approve(address,uint256) 095ea7b3 + function approve(address approved, uint256 tokenId) public { + require(false, stub_error); + approved; + tokenId; + dummy = 0; + } + + // Selector: setApprovalForAll(address,bool) a22cb465 + function setApprovalForAll(address operator, bool approved) public { + require(false, stub_error); + operator; + approved; + dummy = 0; + } + + // Selector: getApproved(uint256) 081812fc + function getApproved(uint256 tokenId) public view returns (address) { + require(false, stub_error); + tokenId; + dummy; + return 0x0000000000000000000000000000000000000000; + } + + // Selector: isApprovedForAll(address,address) e985e9c5 + function isApprovedForAll(address owner, address operator) + public + view + returns (address) + { + require(false, stub_error); + owner; + operator; + dummy; + return 0x0000000000000000000000000000000000000000; + } +} + +contract ERC721Burnable is Dummy { + // Selector: burn(uint256) 42966c68 + function burn(uint256 tokenId) public { + require(false, stub_error); + tokenId; + dummy = 0; + } +} + +contract ERC721Enumerable is Dummy, InlineTotalSupply { + // Selector: tokenByIndex(uint256) 4f6ccce7 + function tokenByIndex(uint256 index) public view returns (uint256) { + require(false, stub_error); + index; + dummy; + return 0; + } + + // Selector: tokenOfOwnerByIndex(address,uint256) 2f745c59 + function tokenOfOwnerByIndex(address owner, uint256 index) + public + view + returns (uint256) + { + require(false, stub_error); + owner; + index; + dummy; + return 0; + } +} + +contract ERC721Metadata is Dummy, InlineNameSymbol { + // Selector: tokenURI(uint256) c87b56dd + function tokenURI(uint256 tokenId) public view returns (string memory) { + require(false, stub_error); + tokenId; + dummy; + return ""; + } +} + +contract ERC721Mintable is Dummy, ERC721MintableEvents { + // Selector: mintingFinished() 05d2035b + function mintingFinished() public view returns (bool) { + require(false, stub_error); + dummy; + return false; + } + + // Selector: mint(address,uint256) 40c10f19 + function mint(address to, uint256 tokenId) public returns (bool) { + require(false, stub_error); + to; + tokenId; + dummy = 0; + return false; + } + + // Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f + function mintWithTokenURI( + address to, + uint256 tokenId, + string memory tokenUri + ) public returns (bool) { + require(false, stub_error); + to; + tokenId; + tokenUri; + dummy = 0; + return false; + } + + // Selector: finishMinting() 7d64bcb4 + function finishMinting() public returns (bool) { + require(false, stub_error); + dummy = 0; + return false; + } +} + +contract ERC721UniqueExtensions is Dummy { + // Selector: transfer(address,uint256) a9059cbb + function transfer(address to, uint256 tokenId) public { + require(false, stub_error); + to; + tokenId; + dummy = 0; + } + + // Selector: nextTokenId() 75794a3c + function nextTokenId() public view returns (uint256) { + require(false, stub_error); + dummy; + return 0; + } + + // Selector: setVariableMetadata(uint256,bytes) d4eac26d + function setVariableMetadata(uint256 tokenId, bytes memory data) public { + require(false, stub_error); + tokenId; + data; + dummy = 0; + } + + // Selector: getVariableMetadata(uint256) e6c5ce6f + function getVariableMetadata(uint256 tokenId) + public + view + returns (bytes memory) + { + require(false, stub_error); + tokenId; + dummy; + return hex""; + } + + // Selector: mintBulk(address,uint256[]) 44a9945e + function mintBulk(address to, uint256[] memory tokenIds) + public + returns (bool) + { + require(false, stub_error); + to; + tokenIds; + dummy = 0; + return false; + } + + // Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006 + function mintBulkWithTokenURI(address to, Tuple0[] memory tokens) + public + returns (bool) + { + require(false, stub_error); + to; + tokens; + dummy = 0; + return false; + } +} + +contract UniqueNFT is + Dummy, + ERC165, + ERC721, + ERC721Metadata, + ERC721Enumerable, + ERC721UniqueExtensions, + ERC721Mintable, + ERC721Burnable +{} --- /dev/null +++ b/pallets/nonfungible/src/weights.rs @@ -0,0 +1,37 @@ +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +pub trait WeightInfo { + fn create_item() -> Weight; + fn create_multiple_items(b: u32) -> Weight; + fn burn_item() -> Weight; + fn transfer() -> Weight; + fn approve() -> Weight; + fn transfer_from() -> Weight; + fn set_variable_metadata() -> Weight; +} + +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + fn create_item() -> Weight {0} + fn create_multiple_items(_b: u32) -> Weight {0} + fn burn_item() -> Weight {0} + fn transfer() -> Weight {0} + fn approve() -> Weight {0} + fn transfer_from() -> Weight {0} + fn set_variable_metadata() -> Weight {0} +} + +impl WeightInfo for () { + fn create_item() -> Weight {0} + fn create_multiple_items(_b: u32) -> Weight {0} + fn burn_item() -> Weight {0} + fn transfer() -> Weight {0} + fn approve() -> Weight {0} + fn transfer_from() -> Weight {0} + fn set_variable_metadata() -> Weight {0} +}