--- /dev/null +++ b/pallets/fungible/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "pallet-fungible" +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' } +ethereum = { default-features = false, version = "0.9.0" } +pallet-evm-coder-substrate = { default-features = false, path = '../evm-coder-substrate' } + +[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", +] +runtime-benchmarks = [] --- /dev/null +++ b/pallets/fungible/src/benchmarking.rs @@ -0,0 +1 @@ +#![cfg(feature = "runtime-benchmarking")] --- /dev/null +++ b/pallets/fungible/src/common.rs @@ -0,0 +1,220 @@ +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::ArithmeticError; +use sp_std::{vec::Vec, vec}; + +use crate::{ + Allowance, Balance, Config, Error, FungibleHandle, Pallet, SelfWeightOf, weights::WeightInfo, +}; + +pub struct CommonWeights(PhantomData); +impl CommonWeightInfo for CommonWeights { + fn create_item() -> Weight { + >::create_item() + } + + fn create_multiple_items(_amount: u32) -> Weight { + Self::create_item() + } + + 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 { + // Error + 0 + } +} + +impl CommonCollectionOperations for FungibleHandle { + fn create_item( + &self, + sender: T::CrossAccountId, + to: T::CrossAccountId, + data: nft_data_structs::CreateItemData, + ) -> DispatchResultWithPostInfo { + match data { + nft_data_structs::CreateItemData::Fungible(data) => with_weight( + >::create_item(self, &sender, (to, data.value)), + >::create_item(), + ), + _ => fail!(>::NotFungibleDataUsedToMintFungibleCollectionToken), + } + } + + fn create_multiple_items( + &self, + sender: T::CrossAccountId, + to: T::CrossAccountId, + data: Vec, + ) -> DispatchResultWithPostInfo { + let mut sum: u128 = 0; + for data in data { + match data { + nft_data_structs::CreateItemData::Fungible(data) => { + sum = sum + .checked_add(data.value) + .ok_or(ArithmeticError::Overflow)?; + } + _ => fail!(>::NotFungibleDataUsedToMintFungibleCollectionToken), + } + } + + with_weight( + >::create_item(self, &sender, (to, sum)), + >::create_item(), + ) + } + + fn burn_item( + &self, + sender: T::CrossAccountId, + token: TokenId, + amount: u128, + ) -> DispatchResultWithPostInfo { + ensure!( + token == TokenId::default(), + >::FungibleItemsHaveNoId + ); + + with_weight( + >::burn(self, &sender, amount), + >::burn_item(), + ) + } + + fn transfer( + &self, + from: T::CrossAccountId, + to: T::CrossAccountId, + token: TokenId, + amount: u128, + ) -> DispatchResultWithPostInfo { + ensure!( + token == TokenId::default(), + >::FungibleItemsHaveNoId + ); + + with_weight( + >::transfer(&self, &from, &to, amount), + >::transfer(), + ) + } + + fn approve( + &self, + sender: T::CrossAccountId, + spender: T::CrossAccountId, + token: TokenId, + amount: u128, + ) -> DispatchResultWithPostInfo { + ensure!( + token == TokenId::default(), + >::FungibleItemsHaveNoId + ); + + with_weight( + >::set_allowance(&self, &sender, &spender, amount), + >::approve(), + ) + } + + fn transfer_from( + &self, + sender: T::CrossAccountId, + from: T::CrossAccountId, + to: T::CrossAccountId, + token: TokenId, + amount: u128, + ) -> DispatchResultWithPostInfo { + ensure!( + token == TokenId::default(), + >::FungibleItemsHaveNoId + ); + + with_weight( + >::transfer_from(&self, &sender, &from, &to, amount), + >::transfer_from(), + ) + } + + fn set_variable_metadata( + &self, + _sender: T::CrossAccountId, + _token: TokenId, + _data: Vec, + ) -> DispatchResultWithPostInfo { + fail!(>::FungibleItemsHaveData) + } + + fn account_tokens(&self, account: T::CrossAccountId) -> Vec { + if >::get((self.id, account.as_sub())) != 0 { + vec![TokenId::default()] + } else { + vec![] + } + } + + fn token_exists(&self, token: TokenId) -> bool { + token == TokenId::default() + } + + fn token_owner(&self, _token: TokenId) -> T::CrossAccountId { + T::CrossAccountId::default() + } + fn const_metadata(&self, _token: TokenId) -> Vec { + Vec::new() + } + fn variable_metadata(&self, _token: TokenId) -> Vec { + Vec::new() + } + + fn collection_tokens(&self) -> u32 { + 1 + } + + fn account_balance(&self, account: T::CrossAccountId) -> u32 { + if >::get((self.id, account.as_sub())) != 0 { + 1 + } else { + 0 + } + } + + fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128 { + if token != TokenId::default() { + return 0; + } + >::get((self.id, account.as_sub())) + } + + fn allowance( + &self, + sender: T::CrossAccountId, + spender: T::CrossAccountId, + token: TokenId, + ) -> u128 { + if token != TokenId::default() { + return 0; + } + >::get((self.id, sender.as_sub(), spender.as_sub())) + } +} --- /dev/null +++ b/pallets/fungible/src/erc.rs @@ -0,0 +1,112 @@ +use core::char::{REPLACEMENT_CHARACTER, decode_utf16}; +use core::convert::TryInto; +use evm_coder::{ToLog, execution::*, generate_stubgen, solidity_interface, types::*}; +use nft_data_structs::CollectionMode; +use pallet_common::erc::CommonEvmHandler; +use sp_core::{H160, U256}; +use sp_std::vec::Vec; +use pallet_common::account::CrossAccountId; +use pallet_common::erc::PrecompileOutput; +use pallet_evm_coder_substrate::{call_internal, dispatch_to_evm}; + +use crate::{Allowance, Balance, Config, FungibleHandle, Pallet, TotalSupply}; + +#[derive(ToLog)] +pub enum ERC20Events { + Transfer { + #[indexed] + from: address, + #[indexed] + to: address, + value: uint256, + }, + Approval { + #[indexed] + owner: address, + #[indexed] + spender: address, + value: uint256, + }, +} + +#[solidity_interface(name = "ERC20", events(ERC20Events))] +impl FungibleHandle { + 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()) + } + fn total_supply(&self) -> Result { + Ok(>::get(self.id).into()) + } + + fn decimals(&self) -> Result { + Ok(if let CollectionMode::Fungible(decimals) = &self.mode { + *decimals + } else { + unreachable!() + }) + } + 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 transfer(&mut self, caller: caller, to: address, amount: uint256) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let to = T::CrossAccountId::from_eth(to); + let amount = amount.try_into().map_err(|_| "amount overflow")?; + + >::transfer(self, &caller, &to, amount).map_err(|_| "transfer error")?; + Ok(true) + } + fn transfer_from( + &mut self, + caller: caller, + from: address, + to: address, + amount: uint256, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let from = T::CrossAccountId::from_eth(from); + let to = T::CrossAccountId::from_eth(to); + let amount = amount.try_into().map_err(|_| "amount overflow")?; + + >::transfer_from(self, &caller, &from, &to, amount) + .map_err(dispatch_to_evm::)?; + Ok(true) + } + fn approve(&mut self, caller: caller, spender: address, amount: uint256) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let spender = T::CrossAccountId::from_eth(spender); + let amount = amount.try_into().map_err(|_| "amount overflow")?; + + >::set_allowance(self, &caller, &spender, amount) + .map_err(dispatch_to_evm::)?; + Ok(true) + } + fn allowance(&self, owner: address, spender: address) -> Result { + let owner = T::CrossAccountId::from_eth(owner); + let spender = T::CrossAccountId::from_eth(spender); + + Ok(>::get((self.id, owner.as_sub(), spender.as_sub())).into()) + } +} + +#[solidity_interface(name = "UniqueFungible", is(ERC20))] +impl FungibleHandle {} + +generate_stubgen!(get_impl, UniqueFungibleCall, true); +generate_stubgen!(gen_iface, UniqueFungibleCall, false); + +impl CommonEvmHandler for FungibleHandle { + const CODE: &'static [u8] = include_bytes!("./stubs/UniqueFungible.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/fungible/src/lib.rs @@ -0,0 +1,366 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use core::ops::Deref; +use frame_support::{ensure}; +use nft_data_structs::{AccessMode, Collection, CollectionId, TokenId}; +use pallet_common::{ + Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, account::CrossAccountId, +}; +use sp_core::H160; +use sp_runtime::{ArithmeticError, DispatchError, DispatchResult}; +use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap}; + +pub use pallet::*; + +use crate::erc::ERC20Events; +pub mod benchmarking; +pub mod common; +pub mod erc; +pub mod weights; + +pub type CreateItemData = (::CrossAccountId, u128); +pub(crate) type SelfWeightOf = ::WeightInfo; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::{Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key}; + use nft_data_structs::CollectionId; + use super::weights::WeightInfo; + + #[pallet::error] + pub enum Error { + /// Not Fungible item data used to mint in Fungible collection. + NotFungibleDataUsedToMintFungibleCollectionToken, + /// Not default id passed as TokenId argument + FungibleItemsHaveNoId, + /// Tried to set data for fungible item + FungibleItemsHaveData, + } + + #[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 TotalSupply = + StorageMap; + + #[pallet::storage] + pub(super) type Balance = StorageNMap< + Key = ( + Key, + Key, + ), + Value = u128, + QueryKind = ValueQuery, + >; + + #[pallet::storage] + pub(super) type Allowance = StorageNMap< + Key = ( + Key, + Key, + Key, + ), + Value = u128, + QueryKind = ValueQuery, + >; +} + +pub struct FungibleHandle(pallet_common::CollectionHandle); +impl FungibleHandle { + pub fn cast(inner: pallet_common::CollectionHandle) -> Self { + Self(inner) + } + pub fn into_inner(self) -> pallet_common::CollectionHandle { + self.0 + } +} +impl Deref for FungibleHandle { + type Target = pallet_common::CollectionHandle; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Pallet { + pub fn init_collection(data: Collection) -> Result { + PalletCommon::init_collection(data) + } + pub fn destroy_collection( + collection: FungibleHandle, + sender: &T::CrossAccountId, + ) -> DispatchResult { + let id = collection.id; + + // ========= + + PalletCommon::destroy_collection(collection.0, sender)?; + + >::remove(id); + >::remove_prefix((id,), None); + >::remove_prefix((id,), None); + Ok(()) + } + + pub fn burn( + collection: &FungibleHandle, + owner: &T::CrossAccountId, + amount: u128, + ) -> DispatchResult { + let total_supply = >::get(collection.id) + .checked_sub(amount) + .ok_or(>::TokenValueTooLow)?; + + let balance = >::get((collection.id, owner.as_sub())) + .checked_sub(amount) + .ok_or(>::TokenValueTooLow)?; + + if collection.access == AccessMode::WhiteList { + collection.check_whitelist(owner)?; + } + + // ========= + + if balance == 0 { + >::remove((collection.id, owner.as_sub())); + } else { + >::insert((collection.id, owner.as_sub()), balance); + } + >::insert(collection.id, total_supply); + + collection.log_infallible(ERC20Events::Transfer { + from: *owner.as_eth(), + to: H160::default(), + value: amount.into(), + }); + >::deposit_event(CommonEvent::ItemDestroyed( + collection.id, + TokenId::default(), + owner.clone(), + amount, + )); + Ok(()) + } + + pub fn transfer( + collection: &FungibleHandle, + from: &T::CrossAccountId, + to: &T::CrossAccountId, + amount: u128, + ) -> DispatchResult { + ensure!( + collection.transfers_enabled, + >::TransferNotAllowed + ); + + 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(amount) + .ok_or(>::TokenValueTooLow)?; + let balance_to = if from != to { + Some( + >::get((collection.id, to.as_sub())) + .checked_add(amount) + .ok_or(ArithmeticError::Overflow)?, + ) + } else { + None + }; + + collection.consume_sstore()?; + collection.consume_sstore()?; + collection.consume_log(2, 32)?; + collection.consume_sstore()?; + + // ========= + + 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); + } + + collection.log_infallible(ERC20Events::Transfer { + from: *from.as_eth(), + to: *to.as_eth(), + value: amount.into(), + }); + >::deposit_event(CommonEvent::Transfer( + collection.id, + TokenId::default(), + from.clone(), + to.clone(), + amount, + )); + Ok(()) + } + + pub fn create_multiple_items( + collection: &FungibleHandle, + 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 (owner, _) in data.iter() { + collection.check_whitelist(owner)?; + } + } + + let mut balances = BTreeMap::new(); + + let total_supply = data + .iter() + .map(|u| u.1) + .try_fold(0u128, |acc, v| acc.checked_add(v)) + .ok_or(ArithmeticError::Overflow)?; + + for (user, amount) in data.into_iter() { + collection.consume_sload()?; + let balance = balances + .entry(user.clone()) + .or_insert_with(|| >::get((collection.id, user.as_sub()))); + *balance = (*balance) + .checked_add(amount) + .ok_or(ArithmeticError::Overflow)?; + } + + collection.consume_sstore()?; + for _ in &balances { + collection.consume_sstore()?; + collection.consume_log(2, 32)?; + collection.consume_sstore()?; + } + + // ========= + + >::insert(collection.id, total_supply); + for (user, amount) in balances { + >::insert((collection.id, user.as_sub()), amount); + + collection.log_infallible(ERC20Events::Transfer { + from: H160::default(), + to: *user.as_eth(), + value: amount.into(), + }); + >::deposit_event(CommonEvent::ItemCreated( + collection.id, + TokenId::default(), + user.clone(), + amount, + )); + } + + Ok(()) + } + + fn set_allowance_unchecked( + collection: &FungibleHandle, + owner: &T::CrossAccountId, + spender: &T::CrossAccountId, + amount: u128, + ) { + >::insert((collection.id, owner.as_sub(), spender.as_sub()), amount); + + collection.log_infallible(ERC20Events::Approval { + owner: *owner.as_eth(), + spender: *spender.as_eth(), + value: amount.into(), + }); + >::deposit_event(CommonEvent::Approved( + collection.id, + TokenId(0), + owner.clone(), + spender.clone(), + amount, + )); + } + + pub fn set_allowance( + collection: &FungibleHandle, + owner: &T::CrossAccountId, + spender: &T::CrossAccountId, + amount: u128, + ) -> DispatchResult { + if collection.access == AccessMode::WhiteList { + collection.check_whitelist(&owner)?; + collection.check_whitelist(&spender)?; + } + + if >::get((collection.id, owner.as_sub())) < amount { + ensure!( + collection.ignores_owned_amount(owner)?, + >::CantApproveMoreThanOwned + ); + } + + // ========= + + Self::set_allowance_unchecked(collection, owner, spender, amount); + Ok(()) + } + + pub fn transfer_from( + collection: &FungibleHandle, + spender: &T::CrossAccountId, + from: &T::CrossAccountId, + to: &T::CrossAccountId, + amount: u128, + ) -> DispatchResult { + if spender == from { + return Self::transfer(collection, from, to, amount); + } + if collection.access == AccessMode::WhiteList { + // `from`, `to` checked in [`transfer`] + collection.check_whitelist(spender)?; + } + + let allowance = >::get((collection.id, from.as_sub(), spender.as_sub())) + .checked_sub(amount); + if allowance.is_none() { + ensure!( + collection.ignores_allowance(spender)?, + >::TokenValueNotEnough + ); + } + + // ========= + + Self::transfer(collection, from, to, amount)?; + if let Some(allowance) = allowance { + Self::set_allowance_unchecked(collection, from, spender, allowance); + } + Ok(()) + } + + /// Delegated to `create_multiple_items` + pub fn create_item( + collection: &FungibleHandle, + sender: &T::CrossAccountId, + data: CreateItemData, + ) -> DispatchResult { + Self::create_multiple_items(collection, sender, vec![data]) + } +} --- /dev/null +++ b/pallets/fungible/src/stubs/UniqueFungible.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: OTHER +// This code is automatically generated + +pragma solidity >=0.8.0 <0.9.0; + +// Common stubs holder +contract Dummy { + uint8 dummy; + string stub_error = "this contract is implemented in native"; +} + +// Inline +contract ERC20Events { + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); +} + +// 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 ERC20 is Dummy, InlineNameSymbol, InlineTotalSupply, ERC20Events { + // Selector: decimals() 313ce567 + function decimals() public view returns (uint8) { + require(false, stub_error); + dummy; + return 0; + } + + // Selector: balanceOf(address) 70a08231 + function balanceOf(address owner) public view returns (uint256) { + require(false, stub_error); + owner; + dummy; + return 0; + } + + // Selector: transfer(address,uint256) a9059cbb + function transfer(address to, uint256 amount) public returns (bool) { + require(false, stub_error); + to; + amount; + dummy = 0; + return false; + } + + // Selector: transferFrom(address,address,uint256) 23b872dd + function transferFrom( + address from, + address to, + uint256 amount + ) public returns (bool) { + require(false, stub_error); + from; + to; + amount; + dummy = 0; + return false; + } + + // Selector: approve(address,uint256) 095ea7b3 + function approve(address spender, uint256 amount) public returns (bool) { + require(false, stub_error); + spender; + amount; + dummy = 0; + return false; + } + + // Selector: allowance(address,address) dd62ed3e + function allowance(address owner, address spender) + public + view + returns (uint256) + { + require(false, stub_error); + owner; + spender; + dummy; + return 0; + } +} + +contract UniqueFungible is Dummy, ERC165, ERC20 {} --- /dev/null +++ b/pallets/fungible/src/weights.rs @@ -0,0 +1,31 @@ +#![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 burn_item() -> Weight; + fn transfer() -> Weight; + fn approve() -> Weight; + fn transfer_from() -> Weight; +} + +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + fn create_item() -> Weight {0} + fn burn_item() -> Weight {0} + fn transfer() -> Weight {0} + fn approve() -> Weight {0} + fn transfer_from() -> Weight {0} +} + +impl WeightInfo for () { + fn create_item() -> Weight {0} + fn burn_item() -> Weight {0} + fn transfer() -> Weight {0} + fn approve() -> Weight {0} + fn transfer_from() -> Weight {0} +}