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
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 #[allow(deprecated)]
83 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
84
85 type Currency: InspectFungible<Self::AccountId>
87 + MutateFungible<Self::AccountId>
88 + HoldMutateFungible<Self::AccountId, Reason = Self::RuntimeHoldReason>;
89
90 type RuntimeHoldReason: From<HoldReason>;
92
93 #[pallet::constant]
96 type Deposit: Get<DepositOf<Self>>;
97
98 type NftCollectionId: Member + Parameter + MaxEncodedLen + Copy + Display;
100
101 type NftId: Member + Parameter + MaxEncodedLen + Copy + Display;
103
104 type AssetBalance: AssetBalance;
106
107 type AssetId: AssetId;
109
110 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 type Nfts: NonFungiblesInspect<
120 Self::AccountId,
121 ItemId = Self::NftId,
122 CollectionId = Self::NftCollectionId,
123 > + Transfer<Self::AccountId>;
124
125 #[pallet::constant]
127 type PalletId: Get<PalletId>;
128
129 #[pallet::constant]
131 type NewAssetSymbol: Get<BoundedVec<u8, Self::StringLimit>>;
132
133 #[pallet::constant]
135 type NewAssetName: Get<BoundedVec<u8, Self::StringLimit>>;
136
137 #[pallet::constant]
139 type StringLimit: Get<u32>;
140
141 #[cfg(feature = "runtime-benchmarks")]
143 type BenchmarkHelper: BenchmarkHelper<Self::AssetId, Self::NftCollectionId, Self::NftId>;
144
145 type WeightInfo: WeightInfo;
147 }
148
149 #[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 NftFractionalized {
164 nft_collection: T::NftCollectionId,
165 nft: T::NftId,
166 fractions: AssetBalanceOf<T>,
167 asset: AssetIdOf<T>,
168 beneficiary: T::AccountId,
169 },
170 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 IncorrectAssetId,
183 NoPermission,
185 NftNotFound,
187 NftNotFractionalized,
189 }
190
191 #[pallet::composite_enum]
193 pub enum HoldReason {
194 #[codec(index = 0)]
196 Fractionalized,
197 }
198
199 #[pallet::call]
200 impl<T: Config> Pallet<T> {
201 #[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 #[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 fn get_pallet_account() -> T::AccountId {
326 T::PalletId::get().into_account_truncating()
327 }
328
329 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 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 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 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 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 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 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}