pallet_nft_fractionalization/
lib.rs1#![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 #[allow(deprecated)]
85 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
86
87 type Currency: InspectFungible<Self::AccountId>
89 + MutateFungible<Self::AccountId>
90 + HoldMutateFungible<Self::AccountId, Reason = Self::RuntimeHoldReason>;
91
92 type RuntimeHoldReason: From<HoldReason>;
94
95 #[pallet::constant]
98 type Deposit: Get<DepositOf<Self>>;
99
100 type NftCollectionId: Member + Parameter + MaxEncodedLen + Copy + Display;
102
103 type NftId: Member + Parameter + MaxEncodedLen + Copy + Display;
105
106 type AssetBalance: AssetBalance;
108
109 type AssetId: AssetId;
111
112 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 type Nfts: NonFungiblesInspect<
122 Self::AccountId,
123 ItemId = Self::NftId,
124 CollectionId = Self::NftCollectionId,
125 > + Transfer<Self::AccountId>;
126
127 #[pallet::constant]
129 type PalletId: Get<PalletId>;
130
131 #[pallet::constant]
133 type NewAssetSymbol: Get<BoundedVec<u8, Self::StringLimit>>;
134
135 #[pallet::constant]
137 type NewAssetName: Get<BoundedVec<u8, Self::StringLimit>>;
138
139 #[pallet::constant]
141 type StringLimit: Get<u32>;
142
143 #[cfg(feature = "runtime-benchmarks")]
145 type BenchmarkHelper: BenchmarkHelper<Self::AssetId, Self::NftCollectionId, Self::NftId>;
146
147 type WeightInfo: WeightInfo;
149 }
150
151 #[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 NftFractionalized {
166 nft_collection: T::NftCollectionId,
167 nft: T::NftId,
168 fractions: AssetBalanceOf<T>,
169 asset: AssetIdOf<T>,
170 beneficiary: T::AccountId,
171 },
172 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 IncorrectAssetId,
185 NoPermission,
187 NftNotFound,
189 NftNotFractionalized,
191 }
192
193 #[pallet::composite_enum]
195 pub enum HoldReason {
196 #[codec(index = 0)]
198 Fractionalized,
199 }
200
201 #[pallet::call]
202 impl<T: Config> Pallet<T> {
203 #[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 #[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 fn get_pallet_account() -> T::AccountId {
332 T::PalletId::get().into_account_truncating()
333 }
334
335 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 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 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 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 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 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 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 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}