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
56#[frame::pallet]
57pub mod pallet {
58	use super::*;
59	use core::fmt::Display;
60	use fungible::{
61		hold::Mutate as HoldMutateFungible, Inspect as InspectFungible, Mutate as MutateFungible,
62	};
63	use fungibles::{
64		metadata::{MetadataDeposit, Mutate as MutateMetadata},
65		Create, Destroy, Inspect, Mutate,
66	};
67	use nonfungibles_v2::{Inspect as NonFungiblesInspect, Transfer};
68	use scale_info::prelude::{format, string::String};
69
70	use tokens::{
71		AssetId, Balance as AssetBalance,
72		Fortitude::Polite,
73		Precision::{BestEffort, Exact},
74		Preservation::{Expendable, Preserve},
75	};
76	#[pallet::pallet]
77	pub struct Pallet<T>(_);
78
79	#[pallet::config]
80	pub trait Config: frame_system::Config {
81		/// The overarching event type.
82		#[allow(deprecated)]
83		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
84
85		/// The currency mechanism, used for paying for deposits.
86		type Currency: InspectFungible<Self::AccountId>
87			+ MutateFungible<Self::AccountId>
88			+ HoldMutateFungible<Self::AccountId, Reason = Self::RuntimeHoldReason>;
89
90		/// Overarching hold reason.
91		type RuntimeHoldReason: From<HoldReason>;
92
93		/// The deposit paid by the user locking an NFT. The deposit is returned to the original NFT
94		/// owner when the asset is unified and the NFT is unlocked.
95		#[pallet::constant]
96		type Deposit: Get<DepositOf<Self>>;
97
98		/// Identifier for the collection of NFT.
99		type NftCollectionId: Member + Parameter + MaxEncodedLen + Copy + Display;
100
101		/// The type used to identify an NFT within a collection.
102		type NftId: Member + Parameter + MaxEncodedLen + Copy + Display;
103
104		/// The type used to describe the amount of fractions converted into assets.
105		type AssetBalance: AssetBalance;
106
107		/// The type used to identify the assets created during fractionalization.
108		type AssetId: AssetId;
109
110		/// Registry for the minted assets.
111		type Assets: Inspect<Self::AccountId, AssetId = Self::AssetId, Balance = Self::AssetBalance>
112			+ Create<Self::AccountId>
113			+ Destroy<Self::AccountId>
114			+ Mutate<Self::AccountId>
115			+ MutateMetadata<Self::AccountId>
116			+ MetadataDeposit<DepositOf<Self>>;
117
118		/// Registry for minted NFTs.
119		type Nfts: NonFungiblesInspect<
120				Self::AccountId,
121				ItemId = Self::NftId,
122				CollectionId = Self::NftCollectionId,
123			> + Transfer<Self::AccountId>;
124
125		/// The pallet's id, used for deriving its sovereign account ID.
126		#[pallet::constant]
127		type PalletId: Get<PalletId>;
128
129		/// The newly created asset's symbol.
130		#[pallet::constant]
131		type NewAssetSymbol: Get<BoundedVec<u8, Self::StringLimit>>;
132
133		/// The newly created asset's name.
134		#[pallet::constant]
135		type NewAssetName: Get<BoundedVec<u8, Self::StringLimit>>;
136
137		/// The maximum length of a name or symbol stored on-chain.
138		#[pallet::constant]
139		type StringLimit: Get<u32>;
140
141		/// A set of helper functions for benchmarking.
142		#[cfg(feature = "runtime-benchmarks")]
143		type BenchmarkHelper: BenchmarkHelper<Self::AssetId, Self::NftCollectionId, Self::NftId>;
144
145		/// Weight information for extrinsics in this pallet.
146		type WeightInfo: WeightInfo;
147	}
148
149	/// Keeps track of the corresponding NFT ID, asset ID and amount minted.
150	#[pallet::storage]
151	pub type NftToAsset<T: Config> = StorageMap<
152		_,
153		Blake2_128Concat,
154		(T::NftCollectionId, T::NftId),
155		Details<AssetIdOf<T>, AssetBalanceOf<T>, DepositOf<T>, T::AccountId>,
156		OptionQuery,
157	>;
158
159	#[pallet::event]
160	#[pallet::generate_deposit(pub(super) fn deposit_event)]
161	pub enum Event<T: Config> {
162		/// An NFT was successfully fractionalized.
163		NftFractionalized {
164			nft_collection: T::NftCollectionId,
165			nft: T::NftId,
166			fractions: AssetBalanceOf<T>,
167			asset: AssetIdOf<T>,
168			beneficiary: T::AccountId,
169		},
170		/// An NFT was successfully returned back.
171		NftUnified {
172			nft_collection: T::NftCollectionId,
173			nft: T::NftId,
174			asset: AssetIdOf<T>,
175			beneficiary: T::AccountId,
176		},
177	}
178
179	#[pallet::error]
180	pub enum Error<T> {
181		/// Asset ID does not correspond to locked NFT.
182		IncorrectAssetId,
183		/// The signing account has no permission to do the operation.
184		NoPermission,
185		/// NFT doesn't exist.
186		NftNotFound,
187		/// NFT has not yet been fractionalised.
188		NftNotFractionalized,
189	}
190
191	/// A reason for the pallet placing a hold on funds.
192	#[pallet::composite_enum]
193	pub enum HoldReason {
194		/// Reserved for a fractionalized NFT.
195		#[codec(index = 0)]
196		Fractionalized,
197	}
198
199	#[pallet::call]
200	impl<T: Config> Pallet<T> {
201		/// Lock the NFT and mint a new fungible asset.
202		///
203		/// The dispatch origin for this call must be Signed.
204		/// The origin must be the owner of the NFT they are trying to lock.
205		///
206		/// `Deposit` funds of sender are reserved.
207		///
208		/// - `nft_collection_id`: The ID used to identify the collection of the NFT.
209		/// Is used within the context of `pallet_nfts`.
210		/// - `nft_id`: The ID used to identify the NFT within the given collection.
211		/// Is used within the context of `pallet_nfts`.
212		/// - `asset_id`: The ID of the new asset. It must not exist.
213		/// Is used within the context of `pallet_assets`.
214		/// - `beneficiary`: The account that will receive the newly created asset.
215		/// - `fractions`: The total issuance of the newly created asset class.
216		///
217		/// Emits `NftFractionalized` event when successful.
218		#[pallet::call_index(0)]
219		#[pallet::weight(T::WeightInfo::fractionalize())]
220		pub fn fractionalize(
221			origin: OriginFor<T>,
222			nft_collection_id: T::NftCollectionId,
223			nft_id: T::NftId,
224			asset_id: AssetIdOf<T>,
225			beneficiary: AccountIdLookupOf<T>,
226			fractions: AssetBalanceOf<T>,
227		) -> DispatchResult {
228			let who = ensure_signed(origin)?;
229			let beneficiary = T::Lookup::lookup(beneficiary)?;
230
231			let nft_owner =
232				T::Nfts::owner(&nft_collection_id, &nft_id).ok_or(Error::<T>::NftNotFound)?;
233			ensure!(nft_owner == who, Error::<T>::NoPermission);
234
235			let pallet_account = Self::get_pallet_account();
236			let deposit = T::Deposit::get();
237			T::Currency::hold(&HoldReason::Fractionalized.into(), &nft_owner, deposit)?;
238			Self::do_lock_nft(nft_collection_id, nft_id)?;
239			Self::do_create_asset(asset_id.clone(), pallet_account.clone())?;
240			Self::do_mint_asset(asset_id.clone(), &beneficiary, fractions)?;
241			Self::do_set_metadata(
242				asset_id.clone(),
243				&who,
244				&pallet_account,
245				&nft_collection_id,
246				&nft_id,
247			)?;
248
249			NftToAsset::<T>::insert(
250				(nft_collection_id, nft_id),
251				Details { asset: asset_id.clone(), fractions, asset_creator: nft_owner, deposit },
252			);
253
254			Self::deposit_event(Event::NftFractionalized {
255				nft_collection: nft_collection_id,
256				nft: nft_id,
257				fractions,
258				asset: asset_id,
259				beneficiary,
260			});
261
262			Ok(())
263		}
264
265		/// Burn the total issuance of the fungible asset and return (unlock) the locked NFT.
266		///
267		/// The dispatch origin for this call must be Signed.
268		///
269		/// `Deposit` funds will be returned to `asset_creator`.
270		///
271		/// - `nft_collection_id`: The ID used to identify the collection of the NFT.
272		/// Is used within the context of `pallet_nfts`.
273		/// - `nft_id`: The ID used to identify the NFT within the given collection.
274		/// Is used within the context of `pallet_nfts`.
275		/// - `asset_id`: The ID of the asset being returned and destroyed. Must match
276		/// the original ID of the created asset, corresponding to the NFT.
277		/// Is used within the context of `pallet_assets`.
278		/// - `beneficiary`: The account that will receive the unified NFT.
279		///
280		/// Emits `NftUnified` event when successful.
281		#[pallet::call_index(1)]
282		#[pallet::weight(T::WeightInfo::unify())]
283		pub fn unify(
284			origin: OriginFor<T>,
285			nft_collection_id: T::NftCollectionId,
286			nft_id: T::NftId,
287			asset_id: AssetIdOf<T>,
288			beneficiary: AccountIdLookupOf<T>,
289		) -> DispatchResult {
290			let who = ensure_signed(origin)?;
291			let beneficiary = T::Lookup::lookup(beneficiary)?;
292
293			NftToAsset::<T>::try_mutate_exists((nft_collection_id, nft_id), |maybe_details| {
294				let details = maybe_details.take().ok_or(Error::<T>::NftNotFractionalized)?;
295				ensure!(details.asset == asset_id, Error::<T>::IncorrectAssetId);
296
297				let deposit = details.deposit;
298				let asset_creator = details.asset_creator;
299				Self::do_burn_asset(asset_id.clone(), &who, details.fractions)?;
300				Self::do_unlock_nft(nft_collection_id, nft_id, &beneficiary)?;
301				T::Currency::release(
302					&HoldReason::Fractionalized.into(),
303					&asset_creator,
304					deposit,
305					BestEffort,
306				)?;
307
308				Self::deposit_event(Event::NftUnified {
309					nft_collection: nft_collection_id,
310					nft: nft_id,
311					asset: asset_id,
312					beneficiary,
313				});
314
315				Ok(())
316			})
317		}
318	}
319
320	impl<T: Config> Pallet<T> {
321		/// The account ID of the pallet.
322		///
323		/// This actually does computation. If you need to keep using it, then make sure you cache
324		/// the value and only call this once.
325		fn get_pallet_account() -> T::AccountId {
326			T::PalletId::get().into_account_truncating()
327		}
328
329		/// Keeps track of the corresponding NFT ID, asset ID and amount minted.
330		pub fn nft_to_asset(
331			key: (T::NftCollectionId, T::NftId),
332		) -> Option<Details<AssetIdOf<T>, AssetBalanceOf<T>, DepositOf<T>, T::AccountId>> {
333			NftToAsset::<T>::get(key)
334		}
335
336		/// Prevent further transferring of NFT.
337		fn do_lock_nft(nft_collection_id: T::NftCollectionId, nft_id: T::NftId) -> DispatchResult {
338			T::Nfts::disable_transfer(&nft_collection_id, &nft_id)
339		}
340
341		/// Remove the transfer lock and transfer the NFT to the account returning the tokens.
342		fn do_unlock_nft(
343			nft_collection_id: T::NftCollectionId,
344			nft_id: T::NftId,
345			account: &T::AccountId,
346		) -> DispatchResult {
347			T::Nfts::enable_transfer(&nft_collection_id, &nft_id)?;
348			T::Nfts::transfer(&nft_collection_id, &nft_id, account)
349		}
350
351		/// Create the new asset.
352		fn do_create_asset(asset_id: AssetIdOf<T>, admin: T::AccountId) -> DispatchResult {
353			T::Assets::create(asset_id, admin, false, One::one())
354		}
355
356		/// Mint the `amount` of tokens with `asset_id` into the beneficiary's account.
357		fn do_mint_asset(
358			asset_id: AssetIdOf<T>,
359			beneficiary: &T::AccountId,
360			amount: AssetBalanceOf<T>,
361		) -> DispatchResult {
362			T::Assets::mint_into(asset_id, beneficiary, amount)?;
363			Ok(())
364		}
365
366		/// Burn tokens from the account.
367		fn do_burn_asset(
368			asset_id: AssetIdOf<T>,
369			account: &T::AccountId,
370			amount: AssetBalanceOf<T>,
371		) -> DispatchResult {
372			T::Assets::burn_from(asset_id.clone(), account, amount, Expendable, Exact, Polite)?;
373			T::Assets::start_destroy(asset_id, None)
374		}
375
376		/// Set the metadata for the newly created asset.
377		fn do_set_metadata(
378			asset_id: AssetIdOf<T>,
379			depositor: &T::AccountId,
380			pallet_account: &T::AccountId,
381			nft_collection_id: &T::NftCollectionId,
382			nft_id: &T::NftId,
383		) -> DispatchResult {
384			let name = format!(
385				"{} {nft_collection_id}-{nft_id}",
386				String::from_utf8_lossy(&T::NewAssetName::get())
387			);
388			let symbol: &[u8] = &T::NewAssetSymbol::get();
389			let existential_deposit = T::Currency::minimum_balance();
390			let pallet_account_balance = T::Currency::balance(&pallet_account);
391
392			if pallet_account_balance < existential_deposit {
393				T::Currency::transfer(&depositor, &pallet_account, existential_deposit, Preserve)?;
394			}
395			let metadata_deposit = T::Assets::calc_metadata_deposit(name.as_bytes(), symbol);
396			if !metadata_deposit.is_zero() {
397				T::Currency::transfer(&depositor, &pallet_account, metadata_deposit, Preserve)?;
398			}
399			T::Assets::set(asset_id, &pallet_account, name.into(), symbol.into(), 0)
400		}
401	}
402}