mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-28 01:38:04 +00:00
bfb241d7f3
* Add missing Cumulus licenses * Typo * Add missing Substrate licenses * Single job checking the sub-repos in steps * Remove dates * Remove dates * Add missing (C) * Update FRAME UI tests Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Update more UI tests Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> --------- Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
408 lines
13 KiB
Rust
408 lines
13 KiB
Rust
// 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.
|
|
|
|
//! # 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::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<T>(_);
|
|
|
|
#[pallet::config]
|
|
pub trait Config: frame_system::Config {
|
|
/// The overarching event type.
|
|
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
|
|
|
/// The currency mechanism, used for paying for deposits.
|
|
type Currency: InspectFungible<Self::AccountId>
|
|
+ MutateFungible<Self::AccountId>
|
|
+ HoldMutateFungible<Self::AccountId, Reason = Self::RuntimeHoldReason>;
|
|
|
|
/// Overarching hold reason.
|
|
type RuntimeHoldReason: From<HoldReason>;
|
|
|
|
/// 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<DepositOf<Self>>;
|
|
|
|
/// 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<Self::AccountId, AssetId = Self::AssetId, Balance = Self::AssetBalance>
|
|
+ Create<Self::AccountId>
|
|
+ Destroy<Self::AccountId>
|
|
+ Mutate<Self::AccountId>
|
|
+ MutateMetadata<Self::AccountId>
|
|
+ MetadataDeposit<DepositOf<Self>>;
|
|
|
|
/// Registry for minted NFTs.
|
|
type Nfts: NonFungiblesInspect<
|
|
Self::AccountId,
|
|
ItemId = Self::NftId,
|
|
CollectionId = Self::NftCollectionId,
|
|
> + Transfer<Self::AccountId>;
|
|
|
|
/// The pallet's id, used for deriving its sovereign account ID.
|
|
#[pallet::constant]
|
|
type PalletId: Get<PalletId>;
|
|
|
|
/// The newly created asset's symbol.
|
|
#[pallet::constant]
|
|
type NewAssetSymbol: Get<BoundedVec<u8, Self::StringLimit>>;
|
|
|
|
/// The newly created asset's name.
|
|
#[pallet::constant]
|
|
type NewAssetName: Get<BoundedVec<u8, Self::StringLimit>>;
|
|
|
|
/// The maximum length of a name or symbol stored on-chain.
|
|
#[pallet::constant]
|
|
type StringLimit: Get<u32>;
|
|
|
|
/// A set of helper functions for benchmarking.
|
|
#[cfg(feature = "runtime-benchmarks")]
|
|
type BenchmarkHelper: BenchmarkHelper<Self::AssetId, Self::NftCollectionId, Self::NftId>;
|
|
|
|
/// 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<T: Config> = StorageMap<
|
|
_,
|
|
Blake2_128Concat,
|
|
(T::NftCollectionId, T::NftId),
|
|
Details<AssetIdOf<T>, AssetBalanceOf<T>, DepositOf<T>, T::AccountId>,
|
|
OptionQuery,
|
|
>;
|
|
|
|
#[pallet::event]
|
|
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
|
pub enum Event<T: Config> {
|
|
/// An NFT was successfully fractionalized.
|
|
NftFractionalized {
|
|
nft_collection: T::NftCollectionId,
|
|
nft: T::NftId,
|
|
fractions: AssetBalanceOf<T>,
|
|
asset: AssetIdOf<T>,
|
|
beneficiary: T::AccountId,
|
|
},
|
|
/// An NFT was successfully returned back.
|
|
NftUnified {
|
|
nft_collection: T::NftCollectionId,
|
|
nft: T::NftId,
|
|
asset: AssetIdOf<T>,
|
|
beneficiary: T::AccountId,
|
|
},
|
|
}
|
|
|
|
#[pallet::error]
|
|
pub enum Error<T> {
|
|
/// 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,
|
|
}
|
|
|
|
/// A reason for the pallet placing a hold on funds.
|
|
#[pallet::composite_enum]
|
|
pub enum HoldReason {
|
|
/// Reserved for a fractionalized NFT.
|
|
#[codec(index = 0)]
|
|
Fractionalized,
|
|
}
|
|
|
|
#[pallet::call]
|
|
impl<T: Config> Pallet<T> {
|
|
/// 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<T>,
|
|
nft_collection_id: T::NftCollectionId,
|
|
nft_id: T::NftId,
|
|
asset_id: AssetIdOf<T>,
|
|
beneficiary: AccountIdLookupOf<T>,
|
|
fractions: AssetBalanceOf<T>,
|
|
) -> 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::<T>::NftNotFound)?;
|
|
ensure!(nft_owner == who, Error::<T>::NoPermission);
|
|
|
|
let pallet_account = Self::get_pallet_account();
|
|
let deposit = T::Deposit::get();
|
|
T::Currency::hold(&HoldReason::Fractionalized.into(), &nft_owner, deposit)?;
|
|
Self::do_lock_nft(nft_collection_id, nft_id)?;
|
|
Self::do_create_asset(asset_id.clone(), pallet_account.clone())?;
|
|
Self::do_mint_asset(asset_id.clone(), &beneficiary, fractions)?;
|
|
Self::do_set_metadata(
|
|
asset_id.clone(),
|
|
&who,
|
|
&pallet_account,
|
|
&nft_collection_id,
|
|
&nft_id,
|
|
)?;
|
|
|
|
NftToAsset::<T>::insert(
|
|
(nft_collection_id, nft_id),
|
|
Details { asset: asset_id.clone(), 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<T>,
|
|
nft_collection_id: T::NftCollectionId,
|
|
nft_id: T::NftId,
|
|
asset_id: AssetIdOf<T>,
|
|
beneficiary: AccountIdLookupOf<T>,
|
|
) -> DispatchResult {
|
|
let who = ensure_signed(origin)?;
|
|
let beneficiary = T::Lookup::lookup(beneficiary)?;
|
|
|
|
NftToAsset::<T>::try_mutate_exists((nft_collection_id, nft_id), |maybe_details| {
|
|
let details = maybe_details.take().ok_or(Error::<T>::NftNotFractionalized)?;
|
|
ensure!(details.asset == asset_id, Error::<T>::IncorrectAssetId);
|
|
|
|
let deposit = details.deposit;
|
|
let asset_creator = details.asset_creator;
|
|
Self::do_burn_asset(asset_id.clone(), &who, details.fractions)?;
|
|
Self::do_unlock_nft(nft_collection_id, nft_id, &beneficiary)?;
|
|
T::Currency::release(
|
|
&HoldReason::Fractionalized.into(),
|
|
&asset_creator,
|
|
deposit,
|
|
BestEffort,
|
|
)?;
|
|
|
|
Self::deposit_event(Event::NftUnified {
|
|
nft_collection: nft_collection_id,
|
|
nft: nft_id,
|
|
asset: asset_id,
|
|
beneficiary,
|
|
});
|
|
|
|
Ok(())
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<T: Config> Pallet<T> {
|
|
/// 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()
|
|
}
|
|
|
|
/// Prevent further transferring of NFT.
|
|
fn do_lock_nft(nft_collection_id: T::NftCollectionId, nft_id: T::NftId) -> DispatchResult {
|
|
T::Nfts::disable_transfer(&nft_collection_id, &nft_id)
|
|
}
|
|
|
|
/// Remove the transfer lock and 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<T>, 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<T>,
|
|
beneficiary: &T::AccountId,
|
|
amount: AssetBalanceOf<T>,
|
|
) -> DispatchResult {
|
|
T::Assets::mint_into(asset_id, beneficiary, amount)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Burn tokens from the account.
|
|
fn do_burn_asset(
|
|
asset_id: AssetIdOf<T>,
|
|
account: &T::AccountId,
|
|
amount: AssetBalanceOf<T>,
|
|
) -> DispatchResult {
|
|
T::Assets::burn_from(asset_id.clone(), 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<T>,
|
|
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)
|
|
}
|
|
}
|
|
}
|