referrerpolicy=no-referrer-when-downgrade

pallet_nft_fractionalization/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! # NFT Fractionalization Pallet
19//!
20//! This pallet provides the basic functionality that should allow users
21//! to leverage partial ownership, transfers, and sales, of illiquid assets,
22//! whether real-world assets represented by their digital twins, or NFTs,
23//! or original NFTs.
24//!
25//! The functionality allows a user to lock an NFT they own, create a new
26//! fungible asset, and mint a set amount of tokens (`fractions`).
27//!
28//! It also allows the user to burn 100% of the asset and to unlock the NFT
29//! into their account.
30//!
31//! ### Functions
32//!
33//! * `fractionalize`: Lock the NFT and create and mint a new fungible asset.
34//! * `unify`: Return 100% of the asset and unlock the NFT.
35
36// Ensure we're `no_std` when compiling for Wasm.
37#![cfg_attr(not(feature = "std"), no_std)]
38
39mod types;
40
41#[cfg(feature = "runtime-benchmarks")]
42mod benchmarking;
43#[cfg(test)]
44pub mod mock;
45#[cfg(test)]
46mod tests;
47
48pub mod weights;
49
50use frame::prelude::*;
51use frame_system::Config as SystemConfig;
52pub use pallet::*;
53pub use types::*;
54pub use weights::WeightInfo;
55
56const LOG_TARGET: &str = "runtime::nft-fractionalization";
57
58#[frame::pallet]
59pub mod pallet {
60	use super::*;
61	use core::fmt::Display;
62	use fungible::{
63		hold::Mutate as HoldMutateFungible, Inspect as InspectFungible, Mutate as MutateFungible,
64	};
65	use fungibles::{
66		metadata::{MetadataDeposit, Mutate as MutateMetadata},
67		Create, Destroy, Inspect, Mutate,
68	};
69	use nonfungibles_v2::{Inspect as NonFungiblesInspect, Transfer};
70	use scale_info::prelude::{format, string::String};
71
72	use tokens::{
73		AssetId, Balance as AssetBalance,
74		Fortitude::Polite,
75		Precision::{BestEffort, Exact},
76		Preservation::{Expendable, Preserve},
77	};
78	#[pallet::pallet]
79	pub struct Pallet<T>(_);
80
81	#[pallet::config]
82	pub trait Config: frame_system::Config {
83		/// The overarching event type.
84		#[allow(deprecated)]
85		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
86
87		/// The currency mechanism, used for paying for deposits.
88		type Currency: InspectFungible<Self::AccountId>
89			+ MutateFungible<Self::AccountId>
90			+ HoldMutateFungible<Self::AccountId, Reason = Self::RuntimeHoldReason>;
91
92		/// Overarching hold reason.
93		type RuntimeHoldReason: From<HoldReason>;
94
95		/// The deposit paid by the user locking an NFT. The deposit is returned to the original NFT
96		/// owner when the asset is unified and the NFT is unlocked.
97		#[pallet::constant]
98		type Deposit: Get<DepositOf<Self>>;
99
100		/// Identifier for the collection of NFT.
101		type NftCollectionId: Member + Parameter + MaxEncodedLen + Copy + Display;
102
103		/// The type used to identify an NFT within a collection.
104		type NftId: Member + Parameter + MaxEncodedLen + Copy + Display;
105
106		/// The type used to describe the amount of fractions converted into assets.
107		type AssetBalance: AssetBalance;
108
109		/// The type used to identify the assets created during fractionalization.
110		type AssetId: AssetId;
111
112		/// Registry for the minted assets.
113		type Assets: Inspect<Self::AccountId, AssetId = Self::AssetId, Balance = Self::AssetBalance>
114			+ Create<Self::AccountId>
115			+ Destroy<Self::AccountId>
116			+ Mutate<Self::AccountId>
117			+ MutateMetadata<Self::AccountId>
118			+ MetadataDeposit<DepositOf<Self>>;
119
120		/// Registry for minted NFTs.
121		type Nfts: NonFungiblesInspect<
122				Self::AccountId,
123				ItemId = Self::NftId,
124				CollectionId = Self::NftCollectionId,
125			> + Transfer<Self::AccountId>;
126
127		/// The pallet's id, used for deriving its sovereign account ID.
128		#[pallet::constant]
129		type PalletId: Get<PalletId>;
130
131		/// The newly created asset's symbol.
132		#[pallet::constant]
133		type NewAssetSymbol: Get<BoundedVec<u8, Self::StringLimit>>;
134
135		/// The newly created asset's name.
136		#[pallet::constant]
137		type NewAssetName: Get<BoundedVec<u8, Self::StringLimit>>;
138
139		/// The maximum length of a name or symbol stored on-chain.
140		#[pallet::constant]
141		type StringLimit: Get<u32>;
142
143		/// A set of helper functions for benchmarking.
144		#[cfg(feature = "runtime-benchmarks")]
145		type BenchmarkHelper: BenchmarkHelper<Self::AssetId, Self::NftCollectionId, Self::NftId>;
146
147		/// Weight information for extrinsics in this pallet.
148		type WeightInfo: WeightInfo;
149	}
150
151	/// Keeps track of the corresponding NFT ID, asset ID and amount minted.
152	#[pallet::storage]
153	pub type NftToAsset<T: Config> = StorageMap<
154		_,
155		Blake2_128Concat,
156		(T::NftCollectionId, T::NftId),
157		Details<AssetIdOf<T>, AssetBalanceOf<T>, DepositOf<T>, T::AccountId>,
158		OptionQuery,
159	>;
160
161	#[pallet::event]
162	#[pallet::generate_deposit(pub(super) fn deposit_event)]
163	pub enum Event<T: Config> {
164		/// An NFT was successfully fractionalized.
165		NftFractionalized {
166			nft_collection: T::NftCollectionId,
167			nft: T::NftId,
168			fractions: AssetBalanceOf<T>,
169			asset: AssetIdOf<T>,
170			beneficiary: T::AccountId,
171		},
172		/// An NFT was successfully returned back.
173		NftUnified {
174			nft_collection: T::NftCollectionId,
175			nft: T::NftId,
176			asset: AssetIdOf<T>,
177			beneficiary: T::AccountId,
178		},
179	}
180
181	#[pallet::error]
182	pub enum Error<T> {
183		/// Asset ID does not correspond to locked NFT.
184		IncorrectAssetId,
185		/// The signing account has no permission to do the operation.
186		NoPermission,
187		/// NFT doesn't exist.
188		NftNotFound,
189		/// NFT has not yet been fractionalised.
190		NftNotFractionalized,
191	}
192
193	/// A reason for the pallet placing a hold on funds.
194	#[pallet::composite_enum]
195	pub enum HoldReason {
196		/// Reserved for a fractionalized NFT.
197		#[codec(index = 0)]
198		Fractionalized,
199	}
200
201	#[pallet::call]
202	impl<T: Config> Pallet<T> {
203		/// Lock the NFT and mint a new fungible asset.
204		///
205		/// The dispatch origin for this call must be Signed.
206		/// The origin must be the owner of the NFT they are trying to lock.
207		///
208		/// `Deposit` funds of sender are reserved.
209		///
210		/// - `nft_collection_id`: The ID used to identify the collection of the NFT.
211		/// Is used within the context of `pallet_nfts`.
212		/// - `nft_id`: The ID used to identify the NFT within the given collection.
213		/// Is used within the context of `pallet_nfts`.
214		/// - `asset_id`: The ID of the new asset. It must not exist.
215		/// Is used within the context of `pallet_assets`.
216		/// - `beneficiary`: The account that will receive the newly created asset.
217		/// - `fractions`: The total issuance of the newly created asset class.
218		///
219		/// Emits `NftFractionalized` event when successful.
220		#[pallet::call_index(0)]
221		#[pallet::weight(T::WeightInfo::fractionalize())]
222		pub fn fractionalize(
223			origin: OriginFor<T>,
224			nft_collection_id: T::NftCollectionId,
225			nft_id: T::NftId,
226			asset_id: AssetIdOf<T>,
227			beneficiary: AccountIdLookupOf<T>,
228			fractions: AssetBalanceOf<T>,
229		) -> DispatchResult {
230			let who = ensure_signed(origin)?;
231			let beneficiary = T::Lookup::lookup(beneficiary)?;
232
233			let nft_owner =
234				T::Nfts::owner(&nft_collection_id, &nft_id).ok_or(Error::<T>::NftNotFound)?;
235			ensure!(nft_owner == who, Error::<T>::NoPermission);
236
237			let pallet_account = Self::get_pallet_account();
238			let deposit = T::Deposit::get();
239			T::Currency::hold(&HoldReason::Fractionalized.into(), &nft_owner, deposit)?;
240			Self::do_lock_nft(nft_collection_id, nft_id)?;
241			Self::do_create_asset(asset_id.clone(), pallet_account.clone())?;
242			Self::do_mint_asset(asset_id.clone(), &beneficiary, fractions)?;
243			Self::do_set_metadata(
244				asset_id.clone(),
245				&who,
246				&pallet_account,
247				&nft_collection_id,
248				&nft_id,
249			)?;
250
251			NftToAsset::<T>::insert(
252				(nft_collection_id, nft_id),
253				Details { asset: asset_id.clone(), fractions, asset_creator: nft_owner, deposit },
254			);
255
256			Self::deposit_event(Event::NftFractionalized {
257				nft_collection: nft_collection_id,
258				nft: nft_id,
259				fractions,
260				asset: asset_id,
261				beneficiary,
262			});
263
264			Ok(())
265		}
266
267		/// Burn the total issuance of the fungible asset and return (unlock) the locked NFT.
268		///
269		/// The asset is destroyed as well, unless it still has asset accounts -- from `touch` --
270		/// or approvals, or the asset registry refuses, in which case anyone can finish the
271		/// destruction later.
272		///
273		/// The dispatch origin for this call must be Signed.
274		///
275		/// `Deposit` funds will be returned to `asset_creator`.
276		///
277		/// - `nft_collection_id`: The ID used to identify the collection of the NFT.
278		/// Is used within the context of `pallet_nfts`.
279		/// - `nft_id`: The ID used to identify the NFT within the given collection.
280		/// Is used within the context of `pallet_nfts`.
281		/// - `asset_id`: The ID of the asset being returned and destroyed. Must match
282		/// the original ID of the created asset, corresponding to the NFT.
283		/// Is used within the context of `pallet_assets`.
284		/// - `beneficiary`: The account that will receive the unified NFT.
285		///
286		/// Emits `NftUnified` event when successful.
287		#[pallet::call_index(1)]
288		#[pallet::weight(T::WeightInfo::unify())]
289		pub fn unify(
290			origin: OriginFor<T>,
291			nft_collection_id: T::NftCollectionId,
292			nft_id: T::NftId,
293			asset_id: AssetIdOf<T>,
294			beneficiary: AccountIdLookupOf<T>,
295		) -> DispatchResult {
296			let who = ensure_signed(origin)?;
297			let beneficiary = T::Lookup::lookup(beneficiary)?;
298
299			NftToAsset::<T>::try_mutate_exists((nft_collection_id, nft_id), |maybe_details| {
300				let details = maybe_details.take().ok_or(Error::<T>::NftNotFractionalized)?;
301				ensure!(details.asset == asset_id, Error::<T>::IncorrectAssetId);
302
303				let deposit = details.deposit;
304				let asset_creator = details.asset_creator;
305				Self::do_burn_asset(asset_id.clone(), &who, details.fractions)?;
306				Self::do_unlock_nft(nft_collection_id, nft_id, &beneficiary)?;
307				T::Currency::release(
308					&HoldReason::Fractionalized.into(),
309					&asset_creator,
310					deposit,
311					BestEffort,
312				)?;
313
314				Self::deposit_event(Event::NftUnified {
315					nft_collection: nft_collection_id,
316					nft: nft_id,
317					asset: asset_id,
318					beneficiary,
319				});
320
321				Ok(())
322			})
323		}
324	}
325
326	impl<T: Config> Pallet<T> {
327		/// The account ID of the pallet.
328		///
329		/// This actually does computation. If you need to keep using it, then make sure you cache
330		/// the value and only call this once.
331		fn get_pallet_account() -> T::AccountId {
332			T::PalletId::get().into_account_truncating()
333		}
334
335		/// Keeps track of the corresponding NFT ID, asset ID and amount minted.
336		pub fn nft_to_asset(
337			key: (T::NftCollectionId, T::NftId),
338		) -> Option<Details<AssetIdOf<T>, AssetBalanceOf<T>, DepositOf<T>, T::AccountId>> {
339			NftToAsset::<T>::get(key)
340		}
341
342		/// Prevent further transferring of NFT.
343		fn do_lock_nft(nft_collection_id: T::NftCollectionId, nft_id: T::NftId) -> DispatchResult {
344			T::Nfts::disable_transfer(&nft_collection_id, &nft_id)
345		}
346
347		/// Remove the transfer lock and transfer the NFT to the account returning the tokens.
348		fn do_unlock_nft(
349			nft_collection_id: T::NftCollectionId,
350			nft_id: T::NftId,
351			account: &T::AccountId,
352		) -> DispatchResult {
353			T::Nfts::enable_transfer(&nft_collection_id, &nft_id)?;
354			T::Nfts::transfer(&nft_collection_id, &nft_id, account)
355		}
356
357		/// Create the new asset.
358		fn do_create_asset(asset_id: AssetIdOf<T>, admin: T::AccountId) -> DispatchResult {
359			T::Assets::create(asset_id, admin, false, One::one())
360		}
361
362		/// Mint the `amount` of tokens with `asset_id` into the beneficiary's account.
363		fn do_mint_asset(
364			asset_id: AssetIdOf<T>,
365			beneficiary: &T::AccountId,
366			amount: AssetBalanceOf<T>,
367		) -> DispatchResult {
368			T::Assets::mint_into(asset_id, beneficiary, amount)?;
369			Ok(())
370		}
371
372		/// Burn tokens from the account and destroy the asset, best effort.
373		fn do_burn_asset(
374			asset_id: AssetIdOf<T>,
375			account: &T::AccountId,
376			amount: AssetBalanceOf<T>,
377		) -> DispatchResult {
378			T::Assets::burn_from(asset_id.clone(), account, amount, Expendable, Exact, Polite)?;
379			T::Assets::start_destroy(asset_id.clone(), None)?;
380			// Finish the destruction, otherwise the asset entry, its metadata and the metadata
381			// deposit stay around. `burn_from` above emptied the only account holding fractions,
382			// so this normally succeeds. But if the asset still has accounts or approvals, which
383			// anyone could create, it fails. So we don't block the unify so the NFT doesn't get
384			// locked for good. We keep old behavior: the asset stays in `Destroying` state and
385			// anyone can clean it up with `destroy_accounts`/`destroy_approvals` +
386			// `finish_destroy`. The storage layer is there because a `Destroy` impl is not
387			// required to be atomic on failure, and we are swallowing that failure.
388			if let Err(error) =
389				storage::with_storage_layer(|| T::Assets::finish_destroy(asset_id.clone()))
390			{
391				log::debug!(
392					target: LOG_TARGET,
393					"asset {asset_id:?} left in destroying state: {error:?}",
394				);
395			}
396
397			Ok(())
398		}
399
400		/// Set the metadata for the newly created asset.
401		fn do_set_metadata(
402			asset_id: AssetIdOf<T>,
403			depositor: &T::AccountId,
404			pallet_account: &T::AccountId,
405			nft_collection_id: &T::NftCollectionId,
406			nft_id: &T::NftId,
407		) -> DispatchResult {
408			let name = format!(
409				"{} {nft_collection_id}-{nft_id}",
410				String::from_utf8_lossy(&T::NewAssetName::get())
411			);
412			let symbol: &[u8] = &T::NewAssetSymbol::get();
413			let existential_deposit = T::Currency::minimum_balance();
414			let pallet_account_balance = T::Currency::balance(&pallet_account);
415
416			if pallet_account_balance < existential_deposit {
417				T::Currency::transfer(&depositor, &pallet_account, existential_deposit, Preserve)?;
418			}
419			let metadata_deposit = T::Assets::calc_metadata_deposit(name.as_bytes(), symbol);
420			if !metadata_deposit.is_zero() {
421				T::Currency::transfer(&depositor, &pallet_account, metadata_deposit, Preserve)?;
422			}
423			T::Assets::set(asset_id, &pallet_account, name.into(), symbol.into(), 0)
424		}
425	}
426}