From 5db875a32d8fc5c36efce39a5b4b78311ebcd70f Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Tue, 12 Oct 2021 16:56:04 +0000 Subject: [PATCH] refactor: split refungible into its own pallet --- --- /dev/null +++ b/pallets/refungible/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "pallet-refungible" +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' } + +[features] +default = ["std"] +std = [ + "frame-support/std", + "frame-system/std", + "sp-runtime/std", + "sp-std/std", + "nft-data-structs/std", + "pallet-common/std", +] +runtime-benchmarks = [] --- /dev/null +++ b/pallets/refungible/src/benchmarking.rs @@ -0,0 +1 @@ +#![cfg(feature = "runtime-benchmarking")] --- /dev/null +++ b/pallets/refungible/src/common.rs @@ -0,0 +1,201 @@ +use core::marker::PhantomData; + +use sp_std::collections::btree_map::BTreeMap; +use frame_support::{dispatch::DispatchResultWithPostInfo, 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, Balance, Config, CreateItemData, DataKind, Error, Owned, Pallet, + RefungibleHandle, 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::ReFungible(data) => Ok(CreateItemData { + const_data: data.const_data, + variable_data: data.variable_data, + users: { + let mut out = BTreeMap::new(); + out.insert(to.clone(), data.pieces); + out + }, + }), + _ => fail!(>::NotRefungibleDataUsedToMintFungibleCollectionToken), + } +} + +impl CommonCollectionOperations for RefungibleHandle { + 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 { + with_weight( + >::burn(self, &sender, token, amount), + >::burn_item(), + ) + } + + fn transfer( + &self, + from: T::CrossAccountId, + to: T::CrossAccountId, + token: TokenId, + amount: u128, + ) -> DispatchResultWithPostInfo { + with_weight( + >::transfer(&self, &from, &to, token, amount), + >::transfer(), + ) + } + + fn approve( + &self, + sender: T::CrossAccountId, + spender: T::CrossAccountId, + token: TokenId, + amount: u128, + ) -> DispatchResultWithPostInfo { + with_weight( + >::set_allowance(&self, &sender, &spender, token, amount), + >::approve(), + ) + } + + fn transfer_from( + &self, + sender: T::CrossAccountId, + from: T::CrossAccountId, + to: T::CrossAccountId, + token: TokenId, + amount: u128, + ) -> DispatchResultWithPostInfo { + with_weight( + >::transfer_from(&self, &sender, &from, &to, token, amount), + >::approve(), + ) + } + + 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 { + T::CrossAccountId::default() + } + 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 { + >::get((self.id, token, account.as_sub())) + } + + fn allowance( + &self, + sender: T::CrossAccountId, + spender: T::CrossAccountId, + token: TokenId, + ) -> u128 { + >::get((self.id, token, sender.as_sub(), spender)) + } +} --- /dev/null +++ b/pallets/refungible/src/erc.rs @@ -0,0 +1,34 @@ +use nft_data_structs::TokenId; +use pallet_common::erc::CommonEvmHandler; + +use crate::{Config, RefungibleHandle}; + +impl CommonEvmHandler for RefungibleHandle { + const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw"); + + fn call( + self, + _source: &sp_core::H160, + _input: &[u8], + _value: sp_core::U256, + ) -> Option { + // TODO: Implement RFT variant of ERC721 + None + } +} + +pub struct RefungibleTokenHandle(pub RefungibleHandle, pub TokenId); + +impl CommonEvmHandler for RefungibleTokenHandle { + const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungibleToken.raw"); + + fn call( + self, + _source: &sp_core::H160, + _input: &[u8], + _value: sp_core::U256, + ) -> Option { + // TODO: Implement RFT variant of ERC20 + None + } +} --- /dev/null +++ b/pallets/refungible/src/lib.rs @@ -0,0 +1,572 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{ensure, BoundedVec}; +use nft_data_structs::{ + AccessMode, CUSTOM_DATA_LIMIT, Collection, CollectionId, CustomDataLimit, + MAX_REFUNGIBLE_PIECES, TokenId, +}; +use pallet_common::{ + Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, account::CrossAccountId, +}; +use sp_runtime::{ArithmeticError, DispatchError, DispatchResult}; +use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap}; +use core::ops::Deref; + +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 users: BTreeMap, +} +pub(crate) type SelfWeightOf = ::WeightInfo; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::{Blake2_128, 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 Refungible item data used to mint in Refungible collection. + NotRefungibleDataUsedToMintFungibleCollectionToken, + /// Maximum refungibility exceeded + WrongRefungiblePieces, + } + + #[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 TotalSupply = StorageNMap< + Key = (Key, Key), + Value = u128, + 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, + // Owner + Key, + ), + Value = u32, + QueryKind = ValueQuery, + >; + + #[pallet::storage] + pub(super) type Balance = StorageNMap< + Key = ( + Key, + Key, + // Owner + Key, + ), + Value = u128, + QueryKind = ValueQuery, + >; + + #[pallet::storage] + pub(super) type Allowance = StorageNMap< + Key = ( + Key, + Key, + // Owner + Key, + // Spender + Key, + ), + Value = u128, + QueryKind = ValueQuery, + >; +} + +pub struct RefungibleHandle(pallet_common::CollectionHandle); +impl RefungibleHandle { + pub fn cast(inner: pallet_common::CollectionHandle) -> Self { + Self(inner) + } + pub fn into_inner(self) -> pallet_common::CollectionHandle { + self.0 + } +} +impl Deref for RefungibleHandle { + type Target = pallet_common::CollectionHandle; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Pallet { + pub fn total_supply(collection: &RefungibleHandle) -> u32 { + >::get(collection.id) - >::get(collection.id) + } + pub fn token_exists(collection: &RefungibleHandle, token: TokenId) -> bool { + >::contains_key((collection.id, token)) + } +} + +// unchecked calls skips any permission checks +impl Pallet { + pub fn init_collection(data: Collection) -> Result { + PalletCommon::init_collection(data) + } + pub fn destroy_collection( + collection: RefungibleHandle, + sender: &T::CrossAccountId, + ) -> DispatchResult { + let id = collection.id; + + // ========= + + PalletCommon::destroy_collection(collection.0, sender)?; + + >::remove(id); + >::remove(id); + >::remove_prefix((id,), None); + >::remove_prefix((id,), None); + >::remove_prefix((id,), None); + >::remove_prefix((id,), None); + Ok(()) + } + + pub fn burn_token(collection: &RefungibleHandle, token_id: TokenId) -> DispatchResult { + let burnt = >::get(collection.id) + .checked_add(1) + .ok_or(ArithmeticError::Overflow)?; + + >::insert(collection.id, burnt); + >::remove_prefix((collection.id, token_id), None); + >::remove((collection.id, token_id)); + >::remove_prefix((collection.id, token_id), None); + >::remove_prefix((collection.id, token_id), None); + // TODO: ERC721 transfer event + return Ok(()); + } + + pub fn burn( + collection: &RefungibleHandle, + owner: &T::CrossAccountId, + token: TokenId, + amount: u128, + ) -> DispatchResult { + let total_supply = >::get((collection.id, token)) + .checked_sub(amount) + .ok_or(>::TokenValueTooLow)?; + + // This was probally last owner of this token? + if total_supply == 0 { + // Ensure user actually owns this amount + ensure!( + >::get((collection.id, token, owner.as_sub())) == amount, + >::TokenValueTooLow + ); + + // ========= + + >::remove((collection.id, owner.as_sub(), token)); + Self::burn_token(collection, token)?; + >::deposit_event(CommonEvent::ItemDestroyed( + collection.id, + token, + owner.clone(), + amount, + )); + return Ok(()); + } + + let balance = >::get((collection.id, token, owner.as_sub())) + .checked_sub(amount) + .ok_or(>::TokenValueTooLow)?; + let account_balance = if balance == 0 { + >::get((collection.id, owner.as_sub())) + .checked_sub(1) + // Should not occur + .ok_or(ArithmeticError::Underflow)? + } else { + 0 + }; + + // ========= + + if balance == 0 { + >::remove((collection.id, token, owner.as_sub())); + >::insert((collection.id, owner.as_sub()), account_balance); + } else { + >::insert((collection.id, token, owner.as_sub()), balance); + } + >::insert((collection.id, token), total_supply); + // TODO: ERC20 transfer event + >::deposit_event(CommonEvent::ItemDestroyed( + collection.id, + token, + owner.clone(), + amount, + )); + Ok(()) + } + + pub fn transfer( + collection: &RefungibleHandle, + from: &T::CrossAccountId, + to: &T::CrossAccountId, + token: TokenId, + 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, token, from.as_sub())) + .checked_sub(amount) + .ok_or(>::TokenValueTooLow)?; + let mut create_target = false; + let from_to_differ = from != to; + let balance_to = if from != to { + let old_balance = >::get((collection.id, token, to.as_sub())); + if old_balance == 0 { + create_target = true; + } + Some( + old_balance + .checked_add(amount) + .ok_or(ArithmeticError::Overflow)?, + ) + } else { + None + }; + + let account_balance_from = if balance_from == 0 { + Some( + >::get((collection.id, from.as_sub())) + .checked_sub(1) + // Should not occur + .ok_or(ArithmeticError::Underflow)?, + ) + } else { + None + }; + // Account data is created in token, AccountBalance should be increased + // But only if from != to as we shouldn't check overflow in this case + let account_balance_to = if create_target && from_to_differ { + let account_balance_to = >::get((collection.id, to.as_sub())) + .checked_add(1) + .ok_or(ArithmeticError::Overflow)?; + ensure!( + account_balance_to < collection.limits.account_token_ownership_limit(), + >::AccountTokenLimitExceeded, + ); + + Some(account_balance_to) + } else { + None + }; + + // ========= + + if let Some(balance_to) = balance_to { + // from != to + if balance_from == 0 { + >::remove((collection.id, token, from.as_sub())); + } else { + >::insert((collection.id, token, from.as_sub()), balance_from); + } + >::insert((collection.id, token, to.as_sub()), balance_to); + if let Some(account_balance_from) = account_balance_from { + >::insert((collection.id, from.as_sub()), account_balance_from); + } + if let Some(account_balance_to) = account_balance_to { + >::insert((collection.id, to.as_sub()), account_balance_to); + } + + >::remove((collection.id, from.as_sub(), token)); + >::insert((collection.id, to.as_sub(), token), true); + } + + // TODO: ERC20 transfer event + >::deposit_event(CommonEvent::Transfer( + collection.id, + token, + from.clone(), + to.clone(), + amount, + )); + Ok(()) + } + + pub fn create_multiple_items( + collection: &RefungibleHandle, + 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() { + for (user, _) in &item.users { + collection.check_whitelist(&user)?; + } + } + } + + for item in data.iter() { + for (owner, _) in item.users.iter() { + >::ensure_correct_receiver(owner)?; + } + } + + // Total pieces per tokens + let totals = data + .iter() + .map(|data| { + Ok(data + .users + .iter() + .map(|u| u.1) + .try_fold(0u128, |acc, v| acc.checked_add(*v)) + .ok_or(ArithmeticError::Overflow)?) + }) + .collect::, DispatchError>>()?; + for total in &totals { + ensure!( + *total <= MAX_REFUNGIBLE_PIECES, + >::WrongRefungiblePieces + ); + } + + let first_token_id = >::get(collection.id); + let tokens_minted = first_token_id + .checked_add(data.len() as u32) + .ok_or(ArithmeticError::Overflow)?; + ensure!( + tokens_minted < collection.limits.token_limit, + >::CollectionTokenLimitExceeded + ); + + let mut balances = BTreeMap::new(); + for data in &data { + for (owner, _) in &data.users { + let balance = balances + .entry(owner.as_sub()) + .or_insert_with(|| >::get((collection.id, owner.as_sub()))); + *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?; + + ensure!( + *balance <= collection.limits.account_token_ownership_limit(), + >::AccountTokenLimitExceeded, + ); + } + } + + // ========= + + >::insert(collection.id, tokens_minted); + for (account, balance) in balances { + >::insert((collection.id, account), balance); + } + for (i, token) in data.into_iter().enumerate() { + let token_id = first_token_id + i as u32; + >::insert((collection.id, token_id), totals[i]); + + if !token.const_data.is_empty() { + >::insert( + (collection.id, token_id, DataKind::Constant), + token.const_data, + ); + } + if !token.variable_data.is_empty() { + >::insert( + (collection.id, token_id, DataKind::Variable), + token.variable_data, + ); + } + for (user, amount) in token.users.into_iter() { + if amount == 0 { + continue; + } + >::insert((collection.id, token_id, user.as_sub()), amount); + >::insert((collection.id, user.as_sub(), TokenId(token_id)), true); + // TODO: ERC20 transfer event + >::deposit_event(CommonEvent::ItemCreated( + collection.id, + TokenId(token_id), + user, + amount, + )); + } + } + Ok(()) + } + + pub fn set_allowance_unchecked( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + spender: &T::CrossAccountId, + token: TokenId, + amount: u128, + ) { + >::insert((collection.id, token, sender.as_sub(), spender), amount); + // TODO: ERC20 approval event + >::deposit_event(CommonEvent::Approved( + collection.id, + token, + sender.clone(), + spender.clone(), + amount, + )) + } + + pub fn set_allowance( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + spender: &T::CrossAccountId, + token: TokenId, + amount: u128, + ) -> DispatchResult { + if collection.access == AccessMode::WhiteList { + collection.check_whitelist(&sender)?; + collection.check_whitelist(&spender)?; + } + + >::ensure_correct_receiver(spender)?; + + if >::get((collection.id, token, sender.as_sub())) < amount { + ensure!( + collection.ignores_owned_amount(sender)?, + >::CantApproveMoreThanOwned + ); + } + + // ========= + + Self::set_allowance_unchecked(collection, sender, spender, token, amount); + Ok(()) + } + + pub fn transfer_from( + collection: &RefungibleHandle, + spender: &T::CrossAccountId, + from: &T::CrossAccountId, + to: &T::CrossAccountId, + token: TokenId, + amount: u128, + ) -> DispatchResult { + if spender == from { + return Self::transfer(collection, from, to, token, amount); + } + if collection.access == AccessMode::WhiteList { + // `from`, `to` checked in [`transfer`] + collection.check_whitelist(spender)?; + } + + let allowance = >::get((collection.id, token, from.as_sub(), &spender)) + .checked_sub(amount); + if allowance.is_none() { + ensure!( + collection.ignores_allowance(spender)?, + >::TokenValueNotEnough + ); + } + + // ========= + + Self::transfer(collection, from, to, token, amount)?; + if let Some(allowance) = allowance { + Self::set_allowance_unchecked(collection, from, spender, token, allowance); + } + Ok(()) + } + + pub fn set_variable_metadata( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + token: TokenId, + data: Vec, + ) -> DispatchResult { + ensure!( + data.len() as u32 <= CUSTOM_DATA_LIMIT, + >::TokenVariableDataLimitExceeded + ); + collection.check_can_update_meta( + sender, + &T::CrossAccountId::from_sub(collection.owner.clone()), + )?; + + collection.consume_sstore()?; + + // ========= + + >::insert((collection.id, token, DataKind::Variable), data); + Ok(()) + } + + /// Delegated to `create_multiple_items` + pub fn create_item( + collection: &RefungibleHandle, + sender: &T::CrossAccountId, + data: CreateItemData, + ) -> DispatchResult { + Self::create_multiple_items(collection, sender, vec![data]) + } +} --- /dev/null +++ b/pallets/refungible/src/stubs/UniqueRefungible.raw @@ -0,0 +1 @@ +TODO --- /dev/null +++ b/pallets/refungible/src/stubs/UniqueRefungibleToken.raw @@ -0,0 +1 @@ +TODO --- /dev/null +++ b/pallets/refungible/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} +} -- gitstuff