git.delta.rocks / unique-network / refs/commits / e83438b2f76b

difftreelog

source

pallets/refungible/src/lib.rs16.7 KiBsourcehistory
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617#![cfg_attr(not(feature = "std"), no_std)]1819use frame_support::{ensure, BoundedVec};20use up_data_structs::{21	AccessMode, CollectionId, CustomDataLimit, MAX_REFUNGIBLE_PIECES, TokenId,22	CreateCollectionData, CreateRefungibleExData,23};24use pallet_common::{25	Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, account::CrossAccountId,26};27use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};28use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};29use core::ops::Deref;30use codec::{Encode, Decode, MaxEncodedLen};31use scale_info::TypeInfo;3233pub use pallet::*;34#[cfg(feature = "runtime-benchmarks")]35pub mod benchmarking;36pub mod common;37pub mod erc;38pub mod weights;39pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;4041#[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]42pub struct ItemData {43	pub const_data: BoundedVec<u8, CustomDataLimit>,44	pub variable_data: BoundedVec<u8, CustomDataLimit>,45}4647#[frame_support::pallet]48pub mod pallet {49	use super::*;50	use frame_support::{Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key};51	use up_data_structs::{CollectionId, TokenId};52	use super::weights::WeightInfo;5354	#[pallet::error]55	pub enum Error<T> {56		/// Not Refungible item data used to mint in Refungible collection.57		NotRefungibleDataUsedToMintFungibleCollectionToken,58		/// Maximum refungibility exceeded59		WrongRefungiblePieces,60	}6162	#[pallet::config]63	pub trait Config: frame_system::Config + pallet_common::Config {64		type WeightInfo: WeightInfo;65	}6667	#[pallet::pallet]68	#[pallet::generate_store(pub(super) trait Store)]69	pub struct Pallet<T>(_);7071	#[pallet::storage]72	pub type TokensMinted<T: Config> =73		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;74	#[pallet::storage]75	pub type TokensBurnt<T: Config> =76		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;7778	#[pallet::storage]79	pub type TokenData<T: Config> = StorageNMap<80		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),81		Value = ItemData,82		QueryKind = ValueQuery,83	>;8485	#[pallet::storage]86	pub type TotalSupply<T: Config> = StorageNMap<87		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),88		Value = u128,89		QueryKind = ValueQuery,90	>;9192	/// Used to enumerate tokens owned by account93	#[pallet::storage]94	pub type Owned<T: Config> = StorageNMap<95		Key = (96			Key<Twox64Concat, CollectionId>,97			Key<Blake2_128Concat, T::CrossAccountId>,98			Key<Twox64Concat, TokenId>,99		),100		Value = bool,101		QueryKind = ValueQuery,102	>;103104	#[pallet::storage]105	pub type AccountBalance<T: Config> = StorageNMap<106		Key = (107			Key<Twox64Concat, CollectionId>,108			// Owner109			Key<Blake2_128Concat, T::CrossAccountId>,110		),111		Value = u32,112		QueryKind = ValueQuery,113	>;114115	#[pallet::storage]116	pub type Balance<T: Config> = StorageNMap<117		Key = (118			Key<Twox64Concat, CollectionId>,119			Key<Twox64Concat, TokenId>,120			// Owner121			Key<Blake2_128Concat, T::CrossAccountId>,122		),123		Value = u128,124		QueryKind = ValueQuery,125	>;126127	#[pallet::storage]128	pub type Allowance<T: Config> = StorageNMap<129		Key = (130			Key<Twox64Concat, CollectionId>,131			Key<Twox64Concat, TokenId>,132			// Owner133			Key<Blake2_128, T::CrossAccountId>,134			// Spender135			Key<Blake2_128Concat, T::CrossAccountId>,136		),137		Value = u128,138		QueryKind = ValueQuery,139	>;140}141142pub struct RefungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);143impl<T: Config> RefungibleHandle<T> {144	pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {145		Self(inner)146	}147	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {148		self.0149	}150}151impl<T: Config> Deref for RefungibleHandle<T> {152	type Target = pallet_common::CollectionHandle<T>;153154	fn deref(&self) -> &Self::Target {155		&self.0156	}157}158159impl<T: Config> Pallet<T> {160	pub fn total_supply(collection: &RefungibleHandle<T>) -> u32 {161		<TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)162	}163	pub fn token_exists(collection: &RefungibleHandle<T>, token: TokenId) -> bool {164		<TotalSupply<T>>::contains_key((collection.id, token))165	}166}167168// unchecked calls skips any permission checks169impl<T: Config> Pallet<T> {170	pub fn init_collection(171		owner: T::AccountId,172		data: CreateCollectionData<T::AccountId>,173	) -> Result<CollectionId, DispatchError> {174		<PalletCommon<T>>::init_collection(owner, data)175	}176	pub fn destroy_collection(177		collection: RefungibleHandle<T>,178		sender: &T::CrossAccountId,179	) -> DispatchResult {180		let id = collection.id;181182		// =========183184		PalletCommon::destroy_collection(collection.0, sender)?;185186		<TokensMinted<T>>::remove(id);187		<TokensBurnt<T>>::remove(id);188		<TokenData<T>>::remove_prefix((id,), None);189		<TotalSupply<T>>::remove_prefix((id,), None);190		<Balance<T>>::remove_prefix((id,), None);191		<Allowance<T>>::remove_prefix((id,), None);192		<Owned<T>>::remove_prefix((id,), None);193		<AccountBalance<T>>::remove_prefix((id,), None);194		Ok(())195	}196197	pub fn burn_token(collection: &RefungibleHandle<T>, token_id: TokenId) -> DispatchResult {198		let burnt = <TokensBurnt<T>>::get(collection.id)199			.checked_add(1)200			.ok_or(ArithmeticError::Overflow)?;201202		<TokensBurnt<T>>::insert(collection.id, burnt);203		<TokenData<T>>::remove((collection.id, token_id));204		<TotalSupply<T>>::remove((collection.id, token_id));205		<Balance<T>>::remove_prefix((collection.id, token_id), None);206		<Allowance<T>>::remove_prefix((collection.id, token_id), None);207		// TODO: ERC721 transfer event208		Ok(())209	}210211	pub fn burn(212		collection: &RefungibleHandle<T>,213		owner: &T::CrossAccountId,214		token: TokenId,215		amount: u128,216	) -> DispatchResult {217		let total_supply = <TotalSupply<T>>::get((collection.id, token))218			.checked_sub(amount)219			.ok_or(<CommonError<T>>::TokenValueTooLow)?;220221		// This was probally last owner of this token?222		if total_supply == 0 {223			// Ensure user actually owns this amount224			ensure!(225				<Balance<T>>::get((collection.id, token, owner)) == amount,226				<CommonError<T>>::TokenValueTooLow227			);228			let account_balance = <AccountBalance<T>>::get((collection.id, owner))229				.checked_sub(1)230				// Should not occur231				.ok_or(ArithmeticError::Underflow)?;232233			// =========234235			<Owned<T>>::remove((collection.id, owner, token));236			<AccountBalance<T>>::insert((collection.id, owner), account_balance);237			Self::burn_token(collection, token)?;238			<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(239				collection.id,240				token,241				owner.clone(),242				amount,243			));244			return Ok(());245		}246247		let balance = <Balance<T>>::get((collection.id, token, owner))248			.checked_sub(amount)249			.ok_or(<CommonError<T>>::TokenValueTooLow)?;250		let account_balance = if balance == 0 {251			<AccountBalance<T>>::get((collection.id, owner))252				.checked_sub(1)253				// Should not occur254				.ok_or(ArithmeticError::Underflow)?255		} else {256			0257		};258259		// =========260261		if balance == 0 {262			<Owned<T>>::remove((collection.id, owner, token));263			<Balance<T>>::remove((collection.id, token, owner));264			<AccountBalance<T>>::insert((collection.id, owner), account_balance);265		} else {266			<Balance<T>>::insert((collection.id, token, owner), balance);267		}268		<TotalSupply<T>>::insert((collection.id, token), total_supply);269		// TODO: ERC20 transfer event270		<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(271			collection.id,272			token,273			owner.clone(),274			amount,275		));276		Ok(())277	}278279	pub fn transfer(280		collection: &RefungibleHandle<T>,281		from: &T::CrossAccountId,282		to: &T::CrossAccountId,283		token: TokenId,284		amount: u128,285	) -> DispatchResult {286		ensure!(287			collection.limits.transfers_enabled(),288			<CommonError<T>>::TransferNotAllowed289		);290291		if collection.access == AccessMode::AllowList {292			collection.check_allowlist(from)?;293			collection.check_allowlist(to)?;294		}295		<PalletCommon<T>>::ensure_correct_receiver(to)?;296297		let balance_from = <Balance<T>>::get((collection.id, token, from))298			.checked_sub(amount)299			.ok_or(<CommonError<T>>::TokenValueTooLow)?;300		let mut create_target = false;301		let from_to_differ = from != to;302		let balance_to = if from != to {303			let old_balance = <Balance<T>>::get((collection.id, token, to));304			if old_balance == 0 {305				create_target = true;306			}307			Some(308				old_balance309					.checked_add(amount)310					.ok_or(ArithmeticError::Overflow)?,311			)312		} else {313			None314		};315316		let account_balance_from = if balance_from == 0 {317			Some(318				<AccountBalance<T>>::get((collection.id, from))319					.checked_sub(1)320					// Should not occur321					.ok_or(ArithmeticError::Underflow)?,322			)323		} else {324			None325		};326		// Account data is created in token, AccountBalance should be increased327		// But only if from != to as we shouldn't check overflow in this case328		let account_balance_to = if create_target && from_to_differ {329			let account_balance_to = <AccountBalance<T>>::get((collection.id, to))330				.checked_add(1)331				.ok_or(ArithmeticError::Overflow)?;332			ensure!(333				account_balance_to < collection.limits.account_token_ownership_limit(),334				<CommonError<T>>::AccountTokenLimitExceeded,335			);336337			Some(account_balance_to)338		} else {339			None340		};341342		// =========343344		if let Some(balance_to) = balance_to {345			// from != to346			if balance_from == 0 {347				<Balance<T>>::remove((collection.id, token, from));348			} else {349				<Balance<T>>::insert((collection.id, token, from), balance_from);350			}351			<Balance<T>>::insert((collection.id, token, to), balance_to);352			if let Some(account_balance_from) = account_balance_from {353				<AccountBalance<T>>::insert((collection.id, from), account_balance_from);354				<Owned<T>>::remove((collection.id, from, token));355			}356			if let Some(account_balance_to) = account_balance_to {357				<AccountBalance<T>>::insert((collection.id, to), account_balance_to);358				<Owned<T>>::insert((collection.id, to, token), true);359			}360		}361362		// TODO: ERC20 transfer event363		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(364			collection.id,365			token,366			from.clone(),367			to.clone(),368			amount,369		));370		Ok(())371	}372373	pub fn create_multiple_items(374		collection: &RefungibleHandle<T>,375		sender: &T::CrossAccountId,376		data: Vec<CreateRefungibleExData<T::CrossAccountId>>,377	) -> DispatchResult {378		if !collection.is_owner_or_admin(sender) {379			ensure!(380				collection.mint_mode,381				<CommonError<T>>::PublicMintingNotAllowed382			);383			collection.check_allowlist(sender)?;384385			for item in data.iter() {386				for user in item.users.keys() {387					collection.check_allowlist(user)?;388				}389			}390		}391392		for item in data.iter() {393			for (owner, _) in item.users.iter() {394				<PalletCommon<T>>::ensure_correct_receiver(owner)?;395			}396		}397398		// Total pieces per tokens399		let totals = data400			.iter()401			.map(|data| {402				Ok(data403					.users404					.iter()405					.map(|u| u.1)406					.try_fold(0u128, |acc, v| acc.checked_add(*v))407					.ok_or(ArithmeticError::Overflow)?)408			})409			.collect::<Result<Vec<_>, DispatchError>>()?;410		for total in &totals {411			ensure!(412				*total <= MAX_REFUNGIBLE_PIECES,413				<Error<T>>::WrongRefungiblePieces414			);415		}416417		let first_token_id = <TokensMinted<T>>::get(collection.id);418		let tokens_minted = first_token_id419			.checked_add(data.len() as u32)420			.ok_or(ArithmeticError::Overflow)?;421		ensure!(422			tokens_minted < collection.limits.token_limit(),423			<CommonError<T>>::CollectionTokenLimitExceeded424		);425426		let mut balances = BTreeMap::new();427		for data in &data {428			for owner in data.users.keys() {429				let balance = balances430					.entry(owner)431					.or_insert_with(|| <AccountBalance<T>>::get((collection.id, owner)));432				*balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;433434				ensure!(435					*balance <= collection.limits.account_token_ownership_limit(),436					<CommonError<T>>::AccountTokenLimitExceeded,437				);438			}439		}440441		// =========442443		<TokensMinted<T>>::insert(collection.id, tokens_minted);444		for (account, balance) in balances {445			<AccountBalance<T>>::insert((collection.id, account), balance);446		}447		for (i, token) in data.into_iter().enumerate() {448			let token_id = first_token_id + i as u32 + 1;449			<TotalSupply<T>>::insert((collection.id, token_id), totals[i]);450451			<TokenData<T>>::insert(452				(collection.id, token_id),453				ItemData {454					const_data: token.const_data,455					variable_data: token.variable_data,456				},457			);458			for (user, amount) in token.users.into_iter() {459				if amount == 0 {460					continue;461				}462				<Balance<T>>::insert((collection.id, token_id, &user), amount);463				<Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);464				// TODO: ERC20 transfer event465				<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(466					collection.id,467					TokenId(token_id),468					user,469					amount,470				));471			}472		}473		Ok(())474	}475476	pub fn set_allowance_unchecked(477		collection: &RefungibleHandle<T>,478		sender: &T::CrossAccountId,479		spender: &T::CrossAccountId,480		token: TokenId,481		amount: u128,482	) {483		if amount == 0 {484			<Allowance<T>>::remove((collection.id, token, sender, spender));485		} else {486			<Allowance<T>>::insert((collection.id, token, sender, spender), amount);487		}488		// TODO: ERC20 approval event489		<PalletCommon<T>>::deposit_event(CommonEvent::Approved(490			collection.id,491			token,492			sender.clone(),493			spender.clone(),494			amount,495		))496	}497498	pub fn set_allowance(499		collection: &RefungibleHandle<T>,500		sender: &T::CrossAccountId,501		spender: &T::CrossAccountId,502		token: TokenId,503		amount: u128,504	) -> DispatchResult {505		if collection.access == AccessMode::AllowList {506			collection.check_allowlist(sender)?;507			collection.check_allowlist(spender)?;508		}509510		<PalletCommon<T>>::ensure_correct_receiver(spender)?;511512		if <Balance<T>>::get((collection.id, token, sender)) < amount {513			ensure!(514				collection.ignores_owned_amount(sender) && Self::token_exists(collection, token),515				<CommonError<T>>::CantApproveMoreThanOwned516			);517		}518519		// =========520521		Self::set_allowance_unchecked(collection, sender, spender, token, amount);522		Ok(())523	}524525	pub fn transfer_from(526		collection: &RefungibleHandle<T>,527		spender: &T::CrossAccountId,528		from: &T::CrossAccountId,529		to: &T::CrossAccountId,530		token: TokenId,531		amount: u128,532	) -> DispatchResult {533		if spender.conv_eq(from) {534			return Self::transfer(collection, from, to, token, amount);535		}536		if collection.access == AccessMode::AllowList {537			// `from`, `to` checked in [`transfer`]538			collection.check_allowlist(spender)?;539		}540541		let allowance =542			<Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);543		if allowance.is_none() {544			ensure!(545				collection.ignores_allowance(spender),546				<CommonError<T>>::ApprovedValueTooLow547			);548		}549550		// =========551552		Self::transfer(collection, from, to, token, amount)?;553		if let Some(allowance) = allowance {554			Self::set_allowance_unchecked(collection, from, spender, token, allowance);555		}556		Ok(())557	}558559	pub fn burn_from(560		collection: &RefungibleHandle<T>,561		spender: &T::CrossAccountId,562		from: &T::CrossAccountId,563		token: TokenId,564		amount: u128,565	) -> DispatchResult {566		if spender.conv_eq(from) {567			return Self::burn(collection, from, token, amount);568		}569		if collection.access == AccessMode::AllowList {570			// `from` checked in [`burn`]571			collection.check_allowlist(spender)?;572		}573574		let allowance =575			<Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);576		if allowance.is_none() {577			ensure!(578				collection.ignores_allowance(spender),579				<CommonError<T>>::ApprovedValueTooLow580			);581		}582583		// =========584585		Self::burn(collection, from, token, amount)?;586		if let Some(allowance) = allowance {587			Self::set_allowance_unchecked(collection, from, spender, token, allowance);588		}589		Ok(())590	}591592	pub fn set_variable_metadata(593		collection: &RefungibleHandle<T>,594		sender: &T::CrossAccountId,595		token: TokenId,596		data: BoundedVec<u8, CustomDataLimit>,597	) -> DispatchResult {598		collection.check_can_update_meta(599			sender,600			&T::CrossAccountId::from_sub(collection.owner.clone()),601		)?;602603		let token_data = <TokenData<T>>::get((collection.id, token));604605		// =========606607		<TokenData<T>>::insert(608			(collection.id, token),609			ItemData {610				variable_data: data,611				..token_data612			},613		);614		Ok(())615	}616617	/// Delegated to `create_multiple_items`618	pub fn create_item(619		collection: &RefungibleHandle<T>,620		sender: &T::CrossAccountId,621		data: CreateRefungibleExData<T::CrossAccountId>,622	) -> DispatchResult {623		Self::create_multiple_items(collection, sender, vec![data])624	}625}