diff --git a/Cargo.lock b/Cargo.lock index b29ed95b87766..f387b5ef95227 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3940,6 +3940,7 @@ dependencies = [ "pallet-message-queue", "pallet-mmr", "pallet-multisig", + "pallet-nft-fractionalization", "pallet-nfts", "pallet-nfts-runtime-api", "pallet-nis", @@ -6648,6 +6649,25 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-nft-fractionalization" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-assets", + "pallet-balances", + "pallet-nfts", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-nfts" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 2b1659d4fb37b..2996fb3858bd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,6 +129,7 @@ members = [ "frame/message-queue", "frame/nfts", "frame/nfts/runtime-api", + "frame/nft-fractionalization", "frame/nomination-pools", "frame/nomination-pools/fuzzer", "frame/nomination-pools/benchmarking", diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index 3909eff2af4f0..9708e1ed9fcdc 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -85,6 +85,7 @@ pallet-mmr = { version = "4.0.0-dev", default-features = false, path = "../../.. pallet-multisig = { version = "4.0.0-dev", default-features = false, path = "../../../frame/multisig" } pallet-nfts = { version = "4.0.0-dev", default-features = false, path = "../../../frame/nfts" } pallet-nfts-runtime-api = { version = "4.0.0-dev", default-features = false, path = "../../../frame/nfts/runtime-api" } +pallet-nft-fractionalization = { version = "4.0.0-dev", default-features = false, path = "../../../frame/nft-fractionalization" } pallet-nomination-pools = { version = "1.0.0", default-features = false, path = "../../../frame/nomination-pools"} pallet-nomination-pools-benchmarking = { version = "1.0.0", default-features = false, optional = true, path = "../../../frame/nomination-pools/benchmarking" } pallet-nomination-pools-runtime-api = { version = "1.0.0-dev", default-features = false, path = "../../../frame/nomination-pools/runtime-api" } @@ -219,6 +220,7 @@ std = [ "pallet-uniques/std", "pallet-nfts/std", "pallet-nfts-runtime-api/std", + "pallet-nft-fractionalization/std", "pallet-vesting/std", "log/std", "frame-try-runtime?/std", @@ -283,6 +285,7 @@ runtime-benchmarks = [ "pallet-utility/runtime-benchmarks", "pallet-uniques/runtime-benchmarks", "pallet-nfts/runtime-benchmarks", + "pallet-nft-fractionalization/runtime-benchmarks", "pallet-vesting/runtime-benchmarks", "pallet-whitelist/runtime-benchmarks", "frame-system-benchmarking/runtime-benchmarks", @@ -349,6 +352,7 @@ try-runtime = [ "pallet-transaction-storage/try-runtime", "pallet-uniques/try-runtime", "pallet-nfts/try-runtime", + "pallet-nft-fractionalization/try-runtime", "pallet-vesting/try-runtime", "pallet-whitelist/try-runtime", ] diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index a2cb64cadd7c5..76143aa1b7b17 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -44,7 +44,7 @@ use frame_support::{ }, ConstantMultiplier, IdentityFee, Weight, }, - PalletId, RuntimeDebug, + BoundedVec, PalletId, RuntimeDebug, }; use frame_system::{ limits::{BlockLength, BlockWeights}, @@ -445,6 +445,8 @@ parameter_types! { pub enum HoldReason { /// The NIS Pallet has reserved it for a non-fungible receipt. Nis, + /// Used by the NFT Fractionalization Pallet. + NftFractionalization, } impl pallet_balances::Config for Runtime { @@ -460,7 +462,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = (); type HoldIdentifier = HoldReason; - type MaxHolds = ConstU32<1>; + type MaxHolds = ConstU32<2>; } parameter_types! { @@ -1612,6 +1614,33 @@ impl pallet_core_fellowship::Config for Runtime { type EvidenceSize = ConstU32<16_384>; } +parameter_types! { + pub const NftFractionalizationPalletId: PalletId = PalletId(*b"fraction"); + pub NewAssetSymbol: BoundedVec = (*b"FRAC").to_vec().try_into().unwrap(); + pub NewAssetName: BoundedVec = (*b"Frac").to_vec().try_into().unwrap(); + pub const NftFractionalizationHoldReason: HoldReason = HoldReason::NftFractionalization; +} + +impl pallet_nft_fractionalization::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Deposit = AssetDeposit; + type Currency = Balances; + type NewAssetSymbol = NewAssetSymbol; + type NewAssetName = NewAssetName; + type StringLimit = StringLimit; + type NftCollectionId = ::CollectionId; + type NftId = ::ItemId; + type AssetBalance = ::Balance; + type AssetId = ::AssetId; + type Assets = Assets; + type Nfts = Nfts; + type PalletId = NftFractionalizationPalletId; + type WeightInfo = pallet_nft_fractionalization::weights::SubstrateWeight; + type HoldReason = NftFractionalizationHoldReason; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + parameter_types! { pub Features: PalletFeatures = PalletFeatures::all_enabled(); pub const MaxAttributesPerCall: u32 = 10; @@ -1829,6 +1858,7 @@ construct_runtime!( Nis: pallet_nis, Uniques: pallet_uniques, Nfts: pallet_nfts, + NftFractionalization: pallet_nft_fractionalization, Salary: pallet_salary, CoreFellowship: pallet_core_fellowship, TransactionStorage: pallet_transaction_storage, @@ -1972,6 +2002,7 @@ mod benches { [pallet_asset_rate, AssetRate] [pallet_uniques, Uniques] [pallet_nfts, Nfts] + [pallet_nft_fractionalization, NftFractionalization] [pallet_utility, Utility] [pallet_vesting, Vesting] [pallet_whitelist, Whitelist] diff --git a/frame/assets/src/functions.rs b/frame/assets/src/functions.rs index d7c5bbe95e9fe..3f32f7b9440a8 100644 --- a/frame/assets/src/functions.rs +++ b/frame/assets/src/functions.rs @@ -962,9 +962,7 @@ impl, I: 'static> Pallet { ensure!(metadata.as_ref().map_or(true, |m| !m.is_frozen), Error::::NoPermission); let old_deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit); - let new_deposit = T::MetadataDepositPerByte::get() - .saturating_mul(((name.len() + symbol.len()) as u32).into()) - .saturating_add(T::MetadataDepositBase::get()); + let new_deposit = Self::calc_metadata_deposit(&name, &symbol); if new_deposit > old_deposit { T::Currency::reserve(from, new_deposit - old_deposit)?; @@ -991,6 +989,13 @@ impl, I: 'static> Pallet { }) } + /// Calculate the metadata deposit for the provided data. + pub(super) fn calc_metadata_deposit(name: &[u8], symbol: &[u8]) -> DepositBalanceOf { + T::MetadataDepositPerByte::get() + .saturating_mul(((name.len() + symbol.len()) as u32).into()) + .saturating_add(T::MetadataDepositBase::get()) + } + /// Returns all the non-zero balances for all assets of the given `account`. pub fn account_balances(account: T::AccountId) -> Vec<(T::AssetId, T::Balance)> { Asset::::iter_keys() diff --git a/frame/assets/src/impl_fungibles.rs b/frame/assets/src/impl_fungibles.rs index 893d74b6aa306..a7df3d154ca8e 100644 --- a/frame/assets/src/impl_fungibles.rs +++ b/frame/assets/src/impl_fungibles.rs @@ -226,6 +226,19 @@ impl, I: 'static> fungibles::metadata::Mutate<:: } } +impl, I: 'static> + fungibles::metadata::MetadataDeposit< + ::AccountId>>::Balance, + > for Pallet +{ + fn calc_metadata_deposit( + name: &[u8], + symbol: &[u8], + ) -> ::AccountId>>::Balance { + Self::calc_metadata_deposit(&name, &symbol) + } +} + impl, I: 'static> fungibles::approvals::Inspect<::AccountId> for Pallet { diff --git a/frame/nft-fractionalization/Cargo.toml b/frame/nft-fractionalization/Cargo.toml new file mode 100644 index 0000000000000..917d9c5d345be --- /dev/null +++ b/frame/nft-fractionalization/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "pallet-nft-fractionalization" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME pallet to convert non-fungible to fungible tokens." +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false } +log = { version = "0.4.17", default-features = false } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } +pallet-assets = { version = "4.0.0-dev", default-features = false, path = "../assets" } +pallet-nfts = { version = "4.0.0-dev", default-features = false, path = "../nfts" } +sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" } +sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" } + +[dev-dependencies] +pallet-balances = { version = "4.0.0-dev", path = "../balances" } +sp-core = { version = "7.0.0", path = "../../primitives/core" } +sp-io = { version = "7.0.0", path = "../../primitives/io" } +sp-std = { version = "5.0.0", path = "../../primitives/std" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-assets/std", + "pallet-nfts/std", + "scale-info/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/nft-fractionalization/README.md b/frame/nft-fractionalization/README.md new file mode 100644 index 0000000000000..180eef22cc46f --- /dev/null +++ b/frame/nft-fractionalization/README.md @@ -0,0 +1,6 @@ +### Lock NFT + +Lock an NFT from `pallet-nfts` and mint fungible assets from `pallet-assets`. + +The NFT gets locked by putting a system-level attribute named `Locked`. This prevents the NFT from being transferred further. +The NFT becomes unlocked when the `Locked` attribute is removed. In order to unify the fungible asset and unlock the NFT, an account must hold the full issuance of the asset the NFT was fractionalised into. Holding less of the fungible asset will not allow the unlocking of the NFT. diff --git a/frame/nft-fractionalization/src/benchmarking.rs b/frame/nft-fractionalization/src/benchmarking.rs new file mode 100644 index 0000000000000..a04e8de12ffe8 --- /dev/null +++ b/frame/nft-fractionalization/src/benchmarking.rs @@ -0,0 +1,135 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Nft fractionalization pallet benchmarking. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use frame_benchmarking::{benchmarks, whitelisted_caller}; +use frame_support::{ + assert_ok, + traits::{ + fungible::{Inspect as InspectFungible, Mutate as MutateFungible}, + tokens::nonfungibles_v2::{Create, Mutate}, + Get, + }, +}; +use frame_system::RawOrigin as SystemOrigin; +use pallet_nfts::{CollectionConfig, CollectionSettings, ItemConfig, MintSettings}; +use sp_runtime::traits::StaticLookup; +use sp_std::prelude::*; + +use crate::Pallet as NftFractionalization; + +type BalanceOf = + <::Currency as InspectFungible<::AccountId>>::Balance; + +type CollectionConfigOf = CollectionConfig< + BalanceOf, + ::BlockNumber, + ::NftCollectionId, +>; + +fn default_collection_config() -> CollectionConfigOf +where + T::Currency: InspectFungible, +{ + CollectionConfig { + settings: CollectionSettings::all_enabled(), + max_supply: None, + mint_settings: MintSettings::default(), + } +} + +fn mint_nft(nft_id: T::NftId) -> (T::AccountId, AccountIdLookupOf) +where + T::Nfts: Create, T::BlockNumber, T::NftCollectionId>> + + Mutate, +{ + let caller: T::AccountId = whitelisted_caller(); + let caller_lookup = T::Lookup::unlookup(caller.clone()); + let ed = T::Currency::minimum_balance(); + let multiplier = BalanceOf::::from(100u8); + T::Currency::set_balance(&caller, ed * multiplier + T::Deposit::get() * multiplier); + + assert_ok!(T::Nfts::create_collection(&caller, &caller, &default_collection_config::())); + let collection = T::BenchmarkHelper::collection(0); + assert_ok!(T::Nfts::mint_into(&collection, &nft_id, &caller, &ItemConfig::default(), true)); + (caller, caller_lookup) +} + +fn assert_last_event(generic_event: ::RuntimeEvent) { + let events = frame_system::Pallet::::events(); + let system_event: ::RuntimeEvent = generic_event.into(); + // compare to the last event record + let frame_system::EventRecord { event, .. } = &events[events.len() - 1]; + assert_eq!(event, &system_event); +} + +benchmarks! { + where_clause { + where + T::Nfts: Create, T::BlockNumber, T::NftCollectionId>> + + Mutate, + } + + fractionalize { + let asset = T::BenchmarkHelper::asset(0); + let collection = T::BenchmarkHelper::collection(0); + let nft = T::BenchmarkHelper::nft(0); + let (caller, caller_lookup) = mint_nft::(nft); + }: _(SystemOrigin::Signed(caller.clone()), collection, nft, asset, caller_lookup, 1000u32.into()) + verify { + assert_last_event::( + Event::NftFractionalized { + nft_collection: collection, + nft, + fractions: 1000u32.into(), + asset, + beneficiary: caller, + }.into() + ); + } + + unify { + let asset = T::BenchmarkHelper::asset(0); + let collection = T::BenchmarkHelper::collection(0); + let nft = T::BenchmarkHelper::nft(0); + let (caller, caller_lookup) = mint_nft::(nft); + NftFractionalization::::fractionalize( + SystemOrigin::Signed(caller.clone()).into(), + collection, + nft, + asset, + caller_lookup.clone(), + 1000u32.into(), + )?; + }: _(SystemOrigin::Signed(caller.clone()), collection, nft, asset, caller_lookup) + verify { + assert_last_event::( + Event::NftUnified { + nft_collection: collection, + nft, + asset, + beneficiary: caller, + }.into() + ); + } + + impl_benchmark_test_suite!(NftFractionalization, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/frame/nft-fractionalization/src/lib.rs b/frame/nft-fractionalization/src/lib.rs new file mode 100644 index 0000000000000..c61719c5c707a --- /dev/null +++ b/frame/nft-fractionalization/src/lib.rs @@ -0,0 +1,389 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # NFT Fractionalization Pallet +//! +//! This pallet provides the basic functionality that should allow users +//! to leverage partial ownership, transfers, and sales, of illiquid assets, +//! whether real-world assets represented by their digital twins, or NFTs, +//! or original NFTs. +//! +//! The functionality allows a user to lock an NFT they own, create a new +//! fungible asset, and mint a set amount of tokens (`fractions`). +//! +//! It also allows the user to burn 100% of the asset and to unlock the NFT +//! into their account. +//! +//! ### Functions +//! +//! * `fractionalize`: Lock the NFT and create and mint a new fungible asset. +//! * `unify`: Return 100% of the asset and unlock the NFT. + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +mod types; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +#[cfg(test)] +pub mod mock; +#[cfg(test)] +mod tests; + +pub mod weights; + +use frame_system::Config as SystemConfig; +pub use pallet::*; +pub use scale_info::Type; +pub use types::*; +pub use weights::WeightInfo; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{ + dispatch::DispatchResult, + ensure, + pallet_prelude::*, + sp_runtime::traits::{AccountIdConversion, StaticLookup}, + traits::{ + fungible::{ + hold::{Inspect as HoldInspectFungible, Mutate as HoldMutateFungible}, + Inspect as InspectFungible, Mutate as MutateFungible, + }, + fungibles::{ + metadata::{MetadataDeposit, Mutate as MutateMetadata}, + Create, Destroy, Inspect, Mutate, + }, + tokens::{ + nonfungibles_v2::{Inspect as NonFungiblesInspect, Transfer}, + AssetId, Balance as AssetBalance, + Fortitude::Polite, + Precision::{BestEffort, Exact}, + Preservation::Preserve, + }, + }, + BoundedVec, PalletId, + }; + use frame_system::pallet_prelude::*; + use scale_info::prelude::{format, string::String}; + use sp_runtime::traits::{One, Zero}; + use sp_std::{fmt::Display, prelude::*}; + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The currency mechanism, used for paying for deposits. + type Currency: InspectFungible + + MutateFungible + + HoldInspectFungible + + HoldMutateFungible; + + #[pallet::constant] + type HoldReason: Get<>::Reason>; + + /// The deposit paid by the user locking an NFT. The deposit is returned to the original NFT + /// owner when the asset is unified and the NFT is unlocked. + #[pallet::constant] + type Deposit: Get>; + + /// Identifier for the collection of NFT. + type NftCollectionId: Member + Parameter + MaxEncodedLen + Copy + Display; + + /// The type used to identify an NFT within a collection. + type NftId: Member + Parameter + MaxEncodedLen + Copy + Display; + + /// The type used to describe the amount of fractions converted into assets. + type AssetBalance: AssetBalance; + + /// The type used to identify the assets created during fractionalization. + type AssetId: AssetId; + + /// Registry for the minted assets. + type Assets: Inspect + + Create + + Destroy + + Mutate + + MutateMetadata + + MetadataDeposit>; + + /// Registry for minted NFTs. + type Nfts: NonFungiblesInspect< + Self::AccountId, + ItemId = Self::NftId, + CollectionId = Self::NftCollectionId, + > + Transfer; + + /// The pallet's id, used for deriving its sovereign account ID. + #[pallet::constant] + type PalletId: Get; + + /// The newly created asset's symbol. + #[pallet::constant] + type NewAssetSymbol: Get>; + + /// The newly created asset's name. + #[pallet::constant] + type NewAssetName: Get>; + + /// The maximum length of a name or symbol stored on-chain. + #[pallet::constant] + type StringLimit: Get; + + /// A set of helper functions for benchmarking. + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper: BenchmarkHelper; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + /// Keeps track of the corresponding NFT ID, asset ID and amount minted. + #[pallet::storage] + #[pallet::getter(fn nft_to_asset)] + pub type NftToAsset = StorageMap< + _, + Blake2_128Concat, + (T::NftCollectionId, T::NftId), + Details, AssetBalanceOf, DepositOf, T::AccountId>, + OptionQuery, + >; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// An NFT was successfully fractionalized. + NftFractionalized { + nft_collection: T::NftCollectionId, + nft: T::NftId, + fractions: AssetBalanceOf, + asset: AssetIdOf, + beneficiary: T::AccountId, + }, + /// An NFT was successfully returned back. + NftUnified { + nft_collection: T::NftCollectionId, + nft: T::NftId, + asset: AssetIdOf, + beneficiary: T::AccountId, + }, + } + + #[pallet::error] + pub enum Error { + /// Asset ID does not correspond to locked NFT. + IncorrectAssetId, + /// The signing account has no permission to do the operation. + NoPermission, + /// NFT doesn't exist. + NftNotFound, + /// NFT has not yet been fractionalised. + NftNotFractionalized, + } + + #[pallet::call] + impl Pallet { + /// Lock the NFT and mint a new fungible asset. + /// + /// The dispatch origin for this call must be Signed. + /// The origin must be the owner of the NFT they are trying to lock. + /// + /// `Deposit` funds of sender are reserved. + /// + /// - `nft_collection_id`: The ID used to identify the collection of the NFT. + /// Is used within the context of `pallet_nfts`. + /// - `nft_id`: The ID used to identify the NFT within the given collection. + /// Is used within the context of `pallet_nfts`. + /// - `asset_id`: The ID of the new asset. It must not exist. + /// Is used within the context of `pallet_assets`. + /// - `beneficiary`: The account that will receive the newly created asset. + /// - `fractions`: The total issuance of the newly created asset class. + /// + /// Emits `NftFractionalized` event when successful. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::fractionalize())] + pub fn fractionalize( + origin: OriginFor, + nft_collection_id: T::NftCollectionId, + nft_id: T::NftId, + asset_id: AssetIdOf, + beneficiary: AccountIdLookupOf, + fractions: AssetBalanceOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let beneficiary = T::Lookup::lookup(beneficiary)?; + + let nft_owner = + T::Nfts::owner(&nft_collection_id, &nft_id).ok_or(Error::::NftNotFound)?; + ensure!(nft_owner == who, Error::::NoPermission); + + let pallet_account = Self::get_pallet_account(); + let deposit = T::Deposit::get(); + T::Currency::hold(&T::HoldReason::get(), &nft_owner, deposit)?; + Self::do_lock_nft(nft_collection_id, nft_id)?; + Self::do_create_asset(asset_id, pallet_account.clone())?; + Self::do_mint_asset(asset_id, &beneficiary, fractions)?; + Self::do_set_metadata(asset_id, &who, &pallet_account, &nft_collection_id, &nft_id)?; + + NftToAsset::::insert( + (nft_collection_id, nft_id), + Details { asset: asset_id, fractions, asset_creator: nft_owner, deposit }, + ); + + Self::deposit_event(Event::NftFractionalized { + nft_collection: nft_collection_id, + nft: nft_id, + fractions, + asset: asset_id, + beneficiary, + }); + + Ok(()) + } + + /// Burn the total issuance of the fungible asset and return (unlock) the locked NFT. + /// + /// The dispatch origin for this call must be Signed. + /// + /// `Deposit` funds will be returned to `asset_creator`. + /// + /// - `nft_collection_id`: The ID used to identify the collection of the NFT. + /// Is used within the context of `pallet_nfts`. + /// - `nft_id`: The ID used to identify the NFT within the given collection. + /// Is used within the context of `pallet_nfts`. + /// - `asset_id`: The ID of the asset being returned and destroyed. Must match + /// the original ID of the created asset, corresponding to the NFT. + /// Is used within the context of `pallet_assets`. + /// - `beneficiary`: The account that will receive the unified NFT. + /// + /// Emits `NftUnified` event when successful. + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::unify())] + pub fn unify( + origin: OriginFor, + nft_collection_id: T::NftCollectionId, + nft_id: T::NftId, + asset_id: AssetIdOf, + beneficiary: AccountIdLookupOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let beneficiary = T::Lookup::lookup(beneficiary)?; + + NftToAsset::::try_mutate_exists((nft_collection_id, nft_id), |maybe_details| { + let details = maybe_details.take().ok_or(Error::::NftNotFractionalized)?; + ensure!(details.asset == asset_id, Error::::IncorrectAssetId); + + let deposit = details.deposit; + let asset_creator = details.asset_creator; + Self::do_burn_asset(asset_id, &who, details.fractions)?; + Self::do_unlock_nft(nft_collection_id, nft_id, &beneficiary)?; + T::Currency::release(&T::HoldReason::get(), &asset_creator, deposit, BestEffort)?; + + Self::deposit_event(Event::NftUnified { + nft_collection: nft_collection_id, + nft: nft_id, + asset: asset_id, + beneficiary, + }); + + Ok(()) + }) + } + } + + impl Pallet { + /// The account ID of the pallet. + /// + /// This actually does computation. If you need to keep using it, then make sure you cache + /// the value and only call this once. + fn get_pallet_account() -> T::AccountId { + T::PalletId::get().into_account_truncating() + } + + /// Transfer the NFT from the account holding that NFT to the pallet's account. + fn do_lock_nft(nft_collection_id: T::NftCollectionId, nft_id: T::NftId) -> DispatchResult { + T::Nfts::disable_transfer(&nft_collection_id, &nft_id) + } + + /// Transfer the NFT to the account returning the tokens. + fn do_unlock_nft( + nft_collection_id: T::NftCollectionId, + nft_id: T::NftId, + account: &T::AccountId, + ) -> DispatchResult { + T::Nfts::enable_transfer(&nft_collection_id, &nft_id)?; + T::Nfts::transfer(&nft_collection_id, &nft_id, account) + } + + /// Create the new asset. + fn do_create_asset(asset_id: AssetIdOf, admin: T::AccountId) -> DispatchResult { + T::Assets::create(asset_id, admin, false, One::one()) + } + + /// Mint the `amount` of tokens with `asset_id` into the beneficiary's account. + fn do_mint_asset( + asset_id: AssetIdOf, + beneficiary: &T::AccountId, + amount: AssetBalanceOf, + ) -> DispatchResult { + T::Assets::mint_into(asset_id, beneficiary, amount)?; + Ok(()) + } + + /// Burn tokens from the account. + fn do_burn_asset( + asset_id: AssetIdOf, + account: &T::AccountId, + amount: AssetBalanceOf, + ) -> DispatchResult { + T::Assets::burn_from(asset_id, account, amount, Exact, Polite)?; + T::Assets::start_destroy(asset_id, None) + } + + /// Set the metadata for the newly created asset. + fn do_set_metadata( + asset_id: AssetIdOf, + depositor: &T::AccountId, + pallet_account: &T::AccountId, + nft_collection_id: &T::NftCollectionId, + nft_id: &T::NftId, + ) -> DispatchResult { + let name = format!( + "{} {nft_collection_id}-{nft_id}", + String::from_utf8_lossy(&T::NewAssetName::get()) + ); + let symbol: &[u8] = &T::NewAssetSymbol::get(); + let existential_deposit = T::Currency::minimum_balance(); + let pallet_account_balance = T::Currency::balance(&pallet_account); + + if pallet_account_balance < existential_deposit { + T::Currency::transfer(&depositor, &pallet_account, existential_deposit, Preserve)?; + } + let metadata_deposit = T::Assets::calc_metadata_deposit(name.as_bytes(), symbol); + if !metadata_deposit.is_zero() { + T::Currency::transfer(&depositor, &pallet_account, metadata_deposit, Preserve)?; + } + T::Assets::set(asset_id, &pallet_account, name.into(), symbol.into(), 0) + } + } +} diff --git a/frame/nft-fractionalization/src/mock.rs b/frame/nft-fractionalization/src/mock.rs new file mode 100644 index 0000000000000..05fbadb039398 --- /dev/null +++ b/frame/nft-fractionalization/src/mock.rs @@ -0,0 +1,202 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test environment for Nft fractionalization pallet. + +use super::*; +use crate as pallet_nft_fractionalization; + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{ + construct_runtime, parameter_types, + traits::{AsEnsureOriginWithArg, ConstU32, ConstU64}, + BoundedVec, PalletId, +}; +use frame_system::EnsureSigned; +use pallet_nfts::PalletFeatures; +use scale_info::TypeInfo; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify}, + MultiSignature, +}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; +type Signature = MultiSignature; +type AccountPublic = ::Signer; +type AccountId = ::AccountId; + +// Configure a mock runtime to test the pallet. +construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + NftFractionalization: pallet_nft_fractionalization, + Assets: pallet_assets, + Balances: pallet_balances, + Nfts: pallet_nfts, + } +); +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +#[derive( + Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, Debug, TypeInfo, +)] +pub enum HoldIdentifier { + NftFractionalization, +} + +impl pallet_balances::Config for Test { + type Balance = u64; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU64<1>; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); + type MaxReserves = ConstU32<50>; + type ReserveIdentifier = [u8; 8]; + type HoldIdentifier = HoldIdentifier; + type MaxHolds = ConstU32<1>; + type FreezeIdentifier = (); + type MaxFreezes = (); +} + +impl pallet_assets::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = u64; + type RemoveItemsLimit = ConstU32<1000>; + type AssetId = u32; + type AssetIdParameter = u32; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type AssetDeposit = ConstU64<1>; + type AssetAccountDeposit = ConstU64<10>; + type MetadataDepositBase = ConstU64<1>; + type MetadataDepositPerByte = ConstU64<1>; + type ApprovalDeposit = ConstU64<1>; + type StringLimit = ConstU32<50>; + type Freezer = (); + type Extra = (); + type CallbackHandle = (); + type WeightInfo = (); + pallet_assets::runtime_benchmarks_enabled! { + type BenchmarkHelper = (); + } +} + +parameter_types! { + pub storage Features: PalletFeatures = PalletFeatures::all_enabled(); +} + +impl pallet_nfts::Config for Test { + type RuntimeEvent = RuntimeEvent; + type CollectionId = u32; + type ItemId = u32; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type Locker = (); + type CollectionDeposit = ConstU64<2>; + type ItemDeposit = ConstU64<1>; + type MetadataDepositBase = ConstU64<1>; + type AttributeDepositBase = ConstU64<1>; + type DepositPerByte = ConstU64<1>; + type StringLimit = ConstU32<50>; + type KeyLimit = ConstU32<50>; + type ValueLimit = ConstU32<50>; + type ApprovalsLimit = ConstU32<10>; + type ItemAttributesApprovalsLimit = ConstU32<2>; + type MaxTips = ConstU32<10>; + type MaxDeadlineDuration = ConstU64<10000>; + type MaxAttributesPerCall = ConstU32<2>; + type Features = Features; + type OffchainSignature = Signature; + type OffchainPublic = AccountPublic; + type WeightInfo = (); + pallet_nfts::runtime_benchmarks_enabled! { + type Helper = (); + } +} + +parameter_types! { + pub const StringLimit: u32 = 50; + pub const NftFractionalizationPalletId: PalletId = PalletId(*b"fraction"); + pub NewAssetSymbol: BoundedVec = (*b"FRAC").to_vec().try_into().unwrap(); + pub NewAssetName: BoundedVec = (*b"Frac").to_vec().try_into().unwrap(); + pub const HoldReason: HoldIdentifier = HoldIdentifier::NftFractionalization; +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type Deposit = ConstU64<1>; + type Currency = Balances; + type NewAssetSymbol = NewAssetSymbol; + type NewAssetName = NewAssetName; + type NftCollectionId = ::CollectionId; + type NftId = ::ItemId; + type AssetBalance = ::Balance; + type AssetId = ::AssetId; + type Assets = Assets; + type Nfts = Nfts; + type PalletId = NftFractionalizationPalletId; + type WeightInfo = (); + type StringLimit = StringLimit; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); + type HoldReason = HoldReason; +} + +// Build genesis storage according to the mock runtime. +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/frame/nft-fractionalization/src/tests.rs b/frame/nft-fractionalization/src/tests.rs new file mode 100644 index 0000000000000..8564b80533e0d --- /dev/null +++ b/frame/nft-fractionalization/src/tests.rs @@ -0,0 +1,278 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests for Nft fractionalization pallet. + +use crate::{mock::*, *}; +use frame_support::{ + assert_noop, assert_ok, + traits::{ + fungible::{hold::Inspect as InspectHold, Mutate as MutateFungible}, + fungibles::{metadata::Inspect, InspectEnumerable}, + }, +}; +use pallet_nfts::CollectionConfig; +use sp_runtime::{DispatchError, ModuleError, TokenError::FundsUnavailable}; + +fn assets() -> Vec { + let mut s: Vec<_> = <::Assets>::asset_ids().collect(); + s.sort(); + s +} + +fn events() -> Vec> { + let result = System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let mock::RuntimeEvent::NftFractionalization(inner) = e { + Some(inner) + } else { + None + } + }) + .collect(); + + System::reset_events(); + + result +} + +type AccountIdOf = ::AccountId; + +fn account(id: u8) -> AccountIdOf { + [id; 32].into() +} + +#[test] +fn fractionalize_should_work() { + new_test_ext().execute_with(|| { + let nft_collection_id = 0; + let nft_id = 0; + let asset_id = 0; + let fractions = 1000; + + Balances::set_balance(&account(1), 100); + Balances::set_balance(&account(2), 100); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + CollectionConfig::default(), + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(account(1)), + nft_collection_id, + nft_id, + account(1), + None, + )); + + assert_ok!(NftFractionalization::fractionalize( + RuntimeOrigin::signed(account(1)), + nft_collection_id, + nft_id, + asset_id, + account(2), + fractions, + )); + assert_eq!(assets(), vec![asset_id]); + assert_eq!(Assets::balance(asset_id, account(2)), fractions); + assert_eq!(Balances::total_balance_on_hold(&account(1)), 2); + assert_eq!(String::from_utf8(Assets::name(0)).unwrap(), "Frac 0-0"); + assert_eq!(String::from_utf8(Assets::symbol(0)).unwrap(), "FRAC"); + assert_eq!(Nfts::owner(nft_collection_id, nft_id), Some(account(1))); + assert_noop!( + Nfts::transfer( + RuntimeOrigin::signed(account(1)), + nft_collection_id, + nft_id, + account(2), + ), + DispatchError::Module(ModuleError { + index: 4, + error: [12, 0, 0, 0], + message: Some("ItemLocked") + }) + ); + + let details = NftToAsset::::get((&nft_collection_id, &nft_id)).unwrap(); + assert_eq!(details.asset, asset_id); + assert_eq!(details.fractions, fractions); + + assert!(events().contains(&Event::::NftFractionalized { + nft_collection: nft_collection_id, + nft: nft_id, + fractions, + asset: asset_id, + beneficiary: account(2), + })); + + let nft_id = nft_id + 1; + assert_noop!( + NftFractionalization::fractionalize( + RuntimeOrigin::signed(account(1)), + nft_collection_id, + nft_id, + asset_id, + account(2), + fractions, + ), + Error::::NftNotFound + ); + + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(account(1)), + nft_collection_id, + nft_id, + account(2), + None + )); + assert_noop!( + NftFractionalization::fractionalize( + RuntimeOrigin::signed(account(1)), + nft_collection_id, + nft_id, + asset_id, + account(2), + fractions, + ), + Error::::NoPermission + ); + }); +} + +#[test] +fn unify_should_work() { + new_test_ext().execute_with(|| { + let nft_collection_id = 0; + let nft_id = 0; + let asset_id = 0; + let fractions = 1000; + + Balances::set_balance(&account(1), 100); + Balances::set_balance(&account(2), 100); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + CollectionConfig::default(), + )); + assert_ok!(Nfts::mint( + RuntimeOrigin::signed(account(1)), + nft_collection_id, + nft_id, + account(1), + None, + )); + assert_ok!(NftFractionalization::fractionalize( + RuntimeOrigin::signed(account(1)), + nft_collection_id, + nft_id, + asset_id, + account(2), + fractions, + )); + + assert_noop!( + NftFractionalization::unify( + RuntimeOrigin::signed(account(2)), + nft_collection_id + 1, + nft_id, + asset_id, + account(1), + ), + Error::::NftNotFractionalized + ); + assert_noop!( + NftFractionalization::unify( + RuntimeOrigin::signed(account(2)), + nft_collection_id, + nft_id, + asset_id + 1, + account(1), + ), + Error::::IncorrectAssetId + ); + + // can't unify the asset a user doesn't hold + assert_noop!( + NftFractionalization::unify( + RuntimeOrigin::signed(account(1)), + nft_collection_id, + nft_id, + asset_id, + account(1), + ), + DispatchError::Token(FundsUnavailable) + ); + + assert_ok!(NftFractionalization::unify( + RuntimeOrigin::signed(account(2)), + nft_collection_id, + nft_id, + asset_id, + account(1), + )); + + assert_eq!(Assets::balance(asset_id, account(2)), 0); + assert_eq!(Balances::reserved_balance(&account(1)), 1); + assert_eq!(Nfts::owner(nft_collection_id, nft_id), Some(account(1))); + assert!(!NftToAsset::::contains_key((&nft_collection_id, &nft_id))); + + assert!(events().contains(&Event::::NftUnified { + nft_collection: nft_collection_id, + nft: nft_id, + asset: asset_id, + beneficiary: account(1), + })); + + // validate we need to hold the full balance to un-fractionalize the NFT + let asset_id = asset_id + 1; + assert_ok!(NftFractionalization::fractionalize( + RuntimeOrigin::signed(account(1)), + nft_collection_id, + nft_id, + asset_id, + account(1), + fractions, + )); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(account(1)), asset_id, account(2), 1)); + assert_eq!(Assets::balance(asset_id, account(1)), fractions - 1); + assert_eq!(Assets::balance(asset_id, account(2)), 1); + assert_noop!( + NftFractionalization::unify( + RuntimeOrigin::signed(account(1)), + nft_collection_id, + nft_id, + asset_id, + account(1), + ), + DispatchError::Token(FundsUnavailable) + ); + + assert_ok!(Assets::transfer(RuntimeOrigin::signed(account(2)), asset_id, account(1), 1)); + assert_ok!(NftFractionalization::unify( + RuntimeOrigin::signed(account(1)), + nft_collection_id, + nft_id, + asset_id, + account(2), + )); + assert_eq!(Nfts::owner(nft_collection_id, nft_id), Some(account(2))); + }); +} diff --git a/frame/nft-fractionalization/src/types.rs b/frame/nft-fractionalization/src/types.rs new file mode 100644 index 0000000000000..cbaaf5f5160d3 --- /dev/null +++ b/frame/nft-fractionalization/src/types.rs @@ -0,0 +1,76 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Various basic types for use in the Nft fractionalization pallet. + +use super::*; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::traits::{fungible::Inspect as FunInspect, fungibles::Inspect}; +use scale_info::TypeInfo; +use sp_runtime::traits::StaticLookup; + +pub type AssetIdOf = <::Assets as Inspect<::AccountId>>::AssetId; +pub type AssetBalanceOf = + <::Assets as Inspect<::AccountId>>::Balance; +pub type DepositOf = + <::Currency as FunInspect<::AccountId>>::Balance; +pub type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; + +/// Stores the details of a fractionalized item. +#[derive(Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +pub struct Details { + /// Minted asset. + pub asset: AssetId, + + /// Number of fractions minted. + pub fractions: Fractions, + + /// Reserved deposit for creating a new asset. + pub deposit: Deposit, + + /// Account that fractionalized an item. + pub asset_creator: AccountId, +} + +/// Benchmark Helper +#[cfg(feature = "runtime-benchmarks")] +pub trait BenchmarkHelper { + /// Returns an asset id from a given integer. + fn asset(id: u32) -> AssetId; + /// Returns a collection id from a given integer. + fn collection(id: u32) -> CollectionId; + /// Returns an nft id from a given integer. + fn nft(id: u32) -> ItemId; +} + +#[cfg(feature = "runtime-benchmarks")] +impl BenchmarkHelper for () +where + AssetId: From, + CollectionId: From, + ItemId: From, +{ + fn asset(id: u32) -> AssetId { + id.into() + } + fn collection(id: u32) -> CollectionId { + id.into() + } + fn nft(id: u32) -> ItemId { + id.into() + } +} diff --git a/frame/nft-fractionalization/src/weights.rs b/frame/nft-fractionalization/src/weights.rs new file mode 100644 index 0000000000000..735b648b82615 --- /dev/null +++ b/frame/nft-fractionalization/src/weights.rs @@ -0,0 +1,184 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for pallet_nft_fractionalization +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-05-19, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `bm3`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// target/production/substrate +// benchmark +// pallet +// --steps=50 +// --repeat=20 +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --json-file=/var/lib/gitlab-runner/builds/zyw4fam_/0/parity/mirrors/substrate/.git/.artifacts/bench.json +// --pallet=pallet_nft_fractionalization +// --chain=dev +// --header=./HEADER-APACHE2 +// --output=./frame/nft-fractionalization/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for pallet_nft_fractionalization. +pub trait WeightInfo { + fn fractionalize() -> Weight; + fn unify() -> Weight; +} + +/// Weights for pallet_nft_fractionalization using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: Nfts Item (r:1 w:0) + /// Proof: Nfts Item (max_values: None, max_size: Some(861), added: 3336, mode: MaxEncodedLen) + /// Storage: Balances Holds (r:1 w:1) + /// Proof: Balances Holds (max_values: None, max_size: Some(83), added: 2558, mode: MaxEncodedLen) + /// Storage: Nfts Collection (r:1 w:1) + /// Proof: Nfts Collection (max_values: None, max_size: Some(84), added: 2559, mode: MaxEncodedLen) + /// Storage: Nfts Attribute (r:1 w:1) + /// Proof: Nfts Attribute (max_values: None, max_size: Some(446), added: 2921, mode: MaxEncodedLen) + /// Storage: Assets Asset (r:1 w:1) + /// Proof: Assets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: Assets Account (r:1 w:1) + /// Proof: Assets Account (max_values: None, max_size: Some(134), added: 2609, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Assets Metadata (r:1 w:1) + /// Proof: Assets Metadata (max_values: None, max_size: Some(140), added: 2615, mode: MaxEncodedLen) + /// Storage: NftFractionalization NftToAsset (r:0 w:1) + /// Proof: NftFractionalization NftToAsset (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + fn fractionalize() -> Weight { + // Proof Size summary in bytes: + // Measured: `609` + // Estimated: `4326` + // Minimum execution time: 177_498_000 picoseconds. + Weight::from_parts(178_803_000, 4326) + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().writes(8_u64)) + } + /// Storage: NftFractionalization NftToAsset (r:1 w:1) + /// Proof: NftFractionalization NftToAsset (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: Assets Asset (r:1 w:1) + /// Proof: Assets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: Assets Account (r:1 w:1) + /// Proof: Assets Account (max_values: None, max_size: Some(134), added: 2609, mode: MaxEncodedLen) + /// Storage: Nfts Attribute (r:1 w:1) + /// Proof: Nfts Attribute (max_values: None, max_size: Some(446), added: 2921, mode: MaxEncodedLen) + /// Storage: Nfts Collection (r:1 w:1) + /// Proof: Nfts Collection (max_values: None, max_size: Some(84), added: 2559, mode: MaxEncodedLen) + /// Storage: Nfts CollectionConfigOf (r:1 w:0) + /// Proof: Nfts CollectionConfigOf (max_values: None, max_size: Some(73), added: 2548, mode: MaxEncodedLen) + /// Storage: Nfts ItemConfigOf (r:1 w:0) + /// Proof: Nfts ItemConfigOf (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: Nfts Item (r:1 w:1) + /// Proof: Nfts Item (max_values: None, max_size: Some(861), added: 3336, mode: MaxEncodedLen) + /// Storage: Balances Holds (r:1 w:1) + /// Proof: Balances Holds (max_values: None, max_size: Some(83), added: 2558, mode: MaxEncodedLen) + /// Storage: Nfts Account (r:0 w:1) + /// Proof: Nfts Account (max_values: None, max_size: Some(88), added: 2563, mode: MaxEncodedLen) + /// Storage: Nfts ItemPriceOf (r:0 w:1) + /// Proof: Nfts ItemPriceOf (max_values: None, max_size: Some(89), added: 2564, mode: MaxEncodedLen) + /// Storage: Nfts PendingSwapOf (r:0 w:1) + /// Proof: Nfts PendingSwapOf (max_values: None, max_size: Some(71), added: 2546, mode: MaxEncodedLen) + fn unify() -> Weight { + // Proof Size summary in bytes: + // Measured: `1421` + // Estimated: `4326` + // Minimum execution time: 130_284_000 picoseconds. + Weight::from_parts(131_122_000, 4326) + .saturating_add(T::DbWeight::get().reads(9_u64)) + .saturating_add(T::DbWeight::get().writes(10_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: Nfts Item (r:1 w:0) + /// Proof: Nfts Item (max_values: None, max_size: Some(861), added: 3336, mode: MaxEncodedLen) + /// Storage: Balances Holds (r:1 w:1) + /// Proof: Balances Holds (max_values: None, max_size: Some(83), added: 2558, mode: MaxEncodedLen) + /// Storage: Nfts Collection (r:1 w:1) + /// Proof: Nfts Collection (max_values: None, max_size: Some(84), added: 2559, mode: MaxEncodedLen) + /// Storage: Nfts Attribute (r:1 w:1) + /// Proof: Nfts Attribute (max_values: None, max_size: Some(446), added: 2921, mode: MaxEncodedLen) + /// Storage: Assets Asset (r:1 w:1) + /// Proof: Assets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: Assets Account (r:1 w:1) + /// Proof: Assets Account (max_values: None, max_size: Some(134), added: 2609, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Assets Metadata (r:1 w:1) + /// Proof: Assets Metadata (max_values: None, max_size: Some(140), added: 2615, mode: MaxEncodedLen) + /// Storage: NftFractionalization NftToAsset (r:0 w:1) + /// Proof: NftFractionalization NftToAsset (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + fn fractionalize() -> Weight { + // Proof Size summary in bytes: + // Measured: `609` + // Estimated: `4326` + // Minimum execution time: 177_498_000 picoseconds. + Weight::from_parts(178_803_000, 4326) + .saturating_add(RocksDbWeight::get().reads(8_u64)) + .saturating_add(RocksDbWeight::get().writes(8_u64)) + } + /// Storage: NftFractionalization NftToAsset (r:1 w:1) + /// Proof: NftFractionalization NftToAsset (max_values: None, max_size: Some(92), added: 2567, mode: MaxEncodedLen) + /// Storage: Assets Asset (r:1 w:1) + /// Proof: Assets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: Assets Account (r:1 w:1) + /// Proof: Assets Account (max_values: None, max_size: Some(134), added: 2609, mode: MaxEncodedLen) + /// Storage: Nfts Attribute (r:1 w:1) + /// Proof: Nfts Attribute (max_values: None, max_size: Some(446), added: 2921, mode: MaxEncodedLen) + /// Storage: Nfts Collection (r:1 w:1) + /// Proof: Nfts Collection (max_values: None, max_size: Some(84), added: 2559, mode: MaxEncodedLen) + /// Storage: Nfts CollectionConfigOf (r:1 w:0) + /// Proof: Nfts CollectionConfigOf (max_values: None, max_size: Some(73), added: 2548, mode: MaxEncodedLen) + /// Storage: Nfts ItemConfigOf (r:1 w:0) + /// Proof: Nfts ItemConfigOf (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: Nfts Item (r:1 w:1) + /// Proof: Nfts Item (max_values: None, max_size: Some(861), added: 3336, mode: MaxEncodedLen) + /// Storage: Balances Holds (r:1 w:1) + /// Proof: Balances Holds (max_values: None, max_size: Some(83), added: 2558, mode: MaxEncodedLen) + /// Storage: Nfts Account (r:0 w:1) + /// Proof: Nfts Account (max_values: None, max_size: Some(88), added: 2563, mode: MaxEncodedLen) + /// Storage: Nfts ItemPriceOf (r:0 w:1) + /// Proof: Nfts ItemPriceOf (max_values: None, max_size: Some(89), added: 2564, mode: MaxEncodedLen) + /// Storage: Nfts PendingSwapOf (r:0 w:1) + /// Proof: Nfts PendingSwapOf (max_values: None, max_size: Some(71), added: 2546, mode: MaxEncodedLen) + fn unify() -> Weight { + // Proof Size summary in bytes: + // Measured: `1421` + // Estimated: `4326` + // Minimum execution time: 130_284_000 picoseconds. + Weight::from_parts(131_122_000, 4326) + .saturating_add(RocksDbWeight::get().reads(9_u64)) + .saturating_add(RocksDbWeight::get().writes(10_u64)) + } +} diff --git a/frame/nfts/src/features/attributes.rs b/frame/nfts/src/features/attributes.rs index 9098679fa9145..8a9bbe8a61de0 100644 --- a/frame/nfts/src/features/attributes.rs +++ b/frame/nfts/src/features/attributes.rs @@ -394,4 +394,19 @@ impl, I: 'static> Pallet { ) -> Result, DispatchError> { Ok(BoundedVec::try_from(value).map_err(|_| Error::::IncorrectData)?) } + + /// A helper method to check whether a system attribute is set for a given item. + pub fn has_system_attribute( + collection: &T::CollectionId, + item: &T::ItemId, + attribute_key: PalletAttributes, + ) -> Result { + let attribute = ( + &collection, + Some(item), + AttributeNamespace::Pallet, + &Self::construct_attribute_key(attribute_key.encode())?, + ); + Ok(Attribute::::contains_key(attribute)) + } } diff --git a/frame/nfts/src/features/transfer.rs b/frame/nfts/src/features/transfer.rs index 00b5d4e76882a..69209e1bb6c4b 100644 --- a/frame/nfts/src/features/transfer.rs +++ b/frame/nfts/src/features/transfer.rs @@ -30,7 +30,12 @@ impl, I: 'static> Pallet { ) -> DispatchResult { let collection_details = Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + ensure!(!T::Locker::is_locked(collection, item), Error::::ItemLocked); + ensure!( + !Self::has_system_attribute(&collection, &item, PalletAttributes::TransferDisabled)?, + Error::::ItemLocked + ); let collection_config = Self::get_collection_config(&collection)?; ensure!( diff --git a/frame/nfts/src/impl_nonfungibles.rs b/frame/nfts/src/impl_nonfungibles.rs index ef6bbe7656ef8..a2bb49a947e82 100644 --- a/frame/nfts/src/impl_nonfungibles.rs +++ b/frame/nfts/src/impl_nonfungibles.rs @@ -117,6 +117,11 @@ impl, I: 'static> Inspect<::AccountId> for Palle /// /// Default implementation is that all items are transferable. fn can_transfer(collection: &Self::CollectionId, item: &Self::ItemId) -> bool { + use PalletAttributes::TransferDisabled; + match Self::has_system_attribute(&collection, &item, TransferDisabled) { + Ok(transfer_disabled) if transfer_disabled => return false, + _ => (), + } match ( CollectionConfigOf::::get(collection), ItemConfigOf::::get(collection, item), @@ -322,6 +327,23 @@ impl, I: 'static> Transfer for Pallet { ) -> DispatchResult { Self::do_transfer(*collection, *item, destination.clone(), |_, _| Ok(())) } + + fn disable_transfer(collection: &Self::CollectionId, item: &Self::ItemId) -> DispatchResult { + >::set_attribute( + collection, + item, + &PalletAttributes::::TransferDisabled.encode(), + &[], + ) + } + + fn enable_transfer(collection: &Self::CollectionId, item: &Self::ItemId) -> DispatchResult { + >::clear_attribute( + collection, + item, + &PalletAttributes::::TransferDisabled.encode(), + ) + } } impl, I: 'static> InspectEnumerable for Pallet { diff --git a/frame/nfts/src/types.rs b/frame/nfts/src/types.rs index fe6d31c12acec..8f36acd286c28 100644 --- a/frame/nfts/src/types.rs +++ b/frame/nfts/src/types.rs @@ -346,6 +346,8 @@ pub struct CancelAttributesApprovalWitness { pub enum PalletAttributes { /// Marks an item as being used in order to claim another item. UsedToClaim(CollectionId), + /// Marks an item as being restricted from transferring. + TransferDisabled, } /// Collection's configuration. diff --git a/frame/support/src/traits/tokens/fungible/regular.rs b/frame/support/src/traits/tokens/fungible/regular.rs index 3476549464032..c0658ad71d2b3 100644 --- a/frame/support/src/traits/tokens/fungible/regular.rs +++ b/frame/support/src/traits/tokens/fungible/regular.rs @@ -58,7 +58,8 @@ pub trait Inspect: Sized { /// The minimum balance any single account may have. fn minimum_balance() -> Self::Balance; - /// Get the total amount of funds whose ultimate bneficial ownership can be determined as `who`. + /// Get the total amount of funds whose ultimate beneficial ownership can be determined as + /// `who`. /// /// This may include funds which are wholly inaccessible to `who`, either temporarily or even /// indefinitely. diff --git a/frame/support/src/traits/tokens/fungibles/metadata.rs b/frame/support/src/traits/tokens/fungibles/metadata.rs index 64f8bf094fb0e..ab310119e5846 100644 --- a/frame/support/src/traits/tokens/fungibles/metadata.rs +++ b/frame/support/src/traits/tokens/fungibles/metadata.rs @@ -39,3 +39,8 @@ pub trait Mutate: Inspect { decimals: u8, ) -> DispatchResult; } + +pub trait MetadataDeposit { + // Returns the required deposit amount for a given metadata. + fn calc_metadata_deposit(name: &[u8], symbol: &[u8]) -> DepositBalance; +} diff --git a/frame/support/src/traits/tokens/misc.rs b/frame/support/src/traits/tokens/misc.rs index 0ba900e95f9b8..d1b17b548fe56 100644 --- a/frame/support/src/traits/tokens/misc.rs +++ b/frame/support/src/traits/tokens/misc.rs @@ -262,8 +262,8 @@ pub trait ConversionFromAssetBalance { ) -> Result; } -/// Trait to handle asset locking mechanism to ensure interactions with the asset can be implemented -/// downstream to extend logic of Uniques current functionality. +/// Trait to handle NFT locking mechanism to ensure interactions with the asset can be implemented +/// downstream to extend logic of Uniques/Nfts current functionality. pub trait Locker { /// Check if the asset should be locked and prevent interactions with the asset from executing. fn is_locked(collection: CollectionId, item: ItemId) -> bool; diff --git a/frame/support/src/traits/tokens/nonfungible_v2.rs b/frame/support/src/traits/tokens/nonfungible_v2.rs index c23bf3e4055b1..c4463e0070f9a 100644 --- a/frame/support/src/traits/tokens/nonfungible_v2.rs +++ b/frame/support/src/traits/tokens/nonfungible_v2.rs @@ -173,10 +173,18 @@ pub trait Mutate: Inspect { } } -/// Trait for transferring a non-fungible item. +/// Trait for transferring and controlling the transfer of non-fungible sets of items. pub trait Transfer: Inspect { /// Transfer `item` into `destination` account. fn transfer(item: &Self::ItemId, destination: &AccountId) -> DispatchResult; + /// Disable the `item` of `collection` transfer. + /// + /// By default, this is not a supported operation. + fn disable_transfer(item: &Self::ItemId) -> DispatchResult; + /// Re-enable the `item` of `collection` transfer. + /// + /// By default, this is not a supported operation. + fn enable_transfer(item: &Self::ItemId) -> DispatchResult; } /// Convert a `nonfungibles` trait implementation into a `nonfungible` trait implementation by @@ -312,4 +320,10 @@ impl< fn transfer(item: &Self::ItemId, destination: &AccountId) -> DispatchResult { >::transfer(&A::get(), item, destination) } + fn disable_transfer(item: &Self::ItemId) -> DispatchResult { + >::disable_transfer(&A::get(), item) + } + fn enable_transfer(item: &Self::ItemId) -> DispatchResult { + >::enable_transfer(&A::get(), item) + } } diff --git a/frame/support/src/traits/tokens/nonfungibles_v2.rs b/frame/support/src/traits/tokens/nonfungibles_v2.rs index 9d32f29becd4c..f4c3c22ea9bef 100644 --- a/frame/support/src/traits/tokens/nonfungibles_v2.rs +++ b/frame/support/src/traits/tokens/nonfungibles_v2.rs @@ -338,4 +338,18 @@ pub trait Transfer: Inspect { item: &Self::ItemId, destination: &AccountId, ) -> DispatchResult; + + /// Disable the `item` of `collection` transfer. + /// + /// By default, this is not a supported operation. + fn disable_transfer(_collection: &Self::CollectionId, _item: &Self::ItemId) -> DispatchResult { + Err(TokenError::Unsupported.into()) + } + + /// Re-enable the `item` of `collection` transfer. + /// + /// By default, this is not a supported operation. + fn enable_transfer(_collection: &Self::CollectionId, _item: &Self::ItemId) -> DispatchResult { + Err(TokenError::Unsupported.into()) + } }