// This file is part of Substrate. // Copyright (C) 2017-2021 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. //! # Assets Module //! //! A simple, secure module for dealing with fungible assets. //! //! ## Overview //! //! The Assets module provides functionality for asset management of fungible asset classes //! with a fixed supply, including: //! //! * Asset Issuance (Minting) //! * Asset Transferal //! * Asset Freezing //! * Asset Destruction (Burning) //! * Delegated Asset Transfers ("Approval API") //! //! To use it in your runtime, you need to implement the assets [`Config`]. //! //! The supported dispatchable functions are documented in the [`Call`] enum. //! //! ### Terminology //! //! * **Admin**: An account ID uniquely privileged to be able to unfreeze (thaw) an account and it's //! assets, as well as forcibly transfer a particular class of assets between arbitrary accounts //! and reduce the balance of a particular class of assets of arbitrary accounts. //! * **Asset issuance/minting**: The creation of a new asset, whose total supply will belong to the //! account that issues the asset. This is a privileged operation. //! * **Asset transfer**: The reduction of the balance of an asset of one account with the //! corresponding increase in the balance of another. //! * **Asset destruction**: The process of reduce the balance of an asset of one account. This is //! a privileged operation. //! * **Fungible asset**: An asset whose units are interchangeable. //! * **Issuer**: An account ID uniquely privileged to be able to mint a particular class of assets. //! * **Freezer**: An account ID uniquely privileged to be able to freeze an account from //! transferring a particular class of assets. //! * **Freezing**: Removing the possibility of an unpermissioned transfer of an asset from a //! particular account. //! * **Non-fungible asset**: An asset for which each unit has unique characteristics. //! * **Owner**: An account ID uniquely privileged to be able to destroy a particular asset class, //! or to set the Issuer, Freezer or Admin of that asset class. //! * **Approval**: The act of allowing an account the permission to transfer some //! balance of asset from the approving account into some third-party destination account. //! * **Sufficiency**: The idea of a minimum-balance of an asset being sufficient to allow the //! account's existence on the system without requiring any other existential-deposit. //! //! ### Goals //! //! The assets system in Substrate is designed to make the following possible: //! //! * Issue a new assets in a permissioned or permissionless way, if permissionless, then with a //! deposit required. //! * Allow accounts to be delegated the ability to transfer assets without otherwise existing //! on-chain (*approvals*). //! * Move assets between accounts. //! * Update the asset's total supply. //! * Allow administrative activities by specially privileged accounts including freezing account //! balances and minting/burning assets. //! //! ## Interface //! //! ### Permissionless Functions //! //! * `create`: Creates a new asset class, taking the required deposit. //! * `transfer`: Transfer sender's assets to another account. //! * `transfer_keep_alive`: Transfer sender's assets to another account, keeping the sender alive. //! * `set_metadata`: Set the metadata of an asset class. //! * `clear_metadata`: Remove the metadata of an asset class. //! * `approve_transfer`: Create or increase an delegated transfer. //! * `cancel_approval`: Rescind a previous approval. //! * `transfer_approved`: Transfer third-party's assets to another account. //! //! ### Permissioned Functions //! //! * `force_create`: Creates a new asset class without taking any deposit. //! * `force_set_metadata`: Set the metadata of an asset class. //! * `force_clear_metadata`: Remove the metadata of an asset class. //! * `force_asset_status`: Alter an asset class's attributes. //! * `force_cancel_approval`: Rescind a previous approval. //! //! ### Privileged Functions //! * `destroy`: Destroys an entire asset class; called by the asset class's Owner. //! * `mint`: Increases the asset balance of an account; called by the asset class's Issuer. //! * `burn`: Decreases the asset balance of an account; called by the asset class's Admin. //! * `force_transfer`: Transfers between arbitrary accounts; called by the asset class's Admin. //! * `freeze`: Disallows further `transfer`s from an account; called by the asset class's Freezer. //! * `thaw`: Allows further `transfer`s from an account; called by the asset class's Admin. //! * `transfer_ownership`: Changes an asset class's Owner; called by the asset class's Owner. //! * `set_team`: Changes an asset class's Admin, Freezer and Issuer; called by the asset class's //! Owner. //! //! Please refer to the [`Call`](./enum.Call.html) enum and its associated variants for documentation on each function. //! //! ### Public Functions //! //! //! * `balance` - Get the asset `id` balance of `who`. //! * `total_supply` - Get the total supply of an asset `id`. //! //! Please refer to the [`Module`](./struct.Module.html) struct for details on publicly available functions. //! //! ## Related Modules //! //! * [`System`](../frame_system/index.html) //! * [`Support`](../frame_support/index.html) // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] pub mod weights; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; #[cfg(test)] pub mod mock; #[cfg(test)] mod tests; use sp_std::prelude::*; use sp_runtime::{ RuntimeDebug, traits::{ AtLeast32BitUnsigned, Zero, StaticLookup, Saturating, CheckedSub, CheckedAdd, } }; use codec::{Encode, Decode, HasCompact}; use frame_support::{ensure, dispatch::{DispatchError, DispatchResult}}; use frame_support::traits::{Currency, ReservableCurrency, BalanceStatus::Reserved, Fungibles}; use frame_system::Config as SystemConfig; pub use weights::WeightInfo; pub use pallet::*; impl Fungibles<::AccountId> for Pallet { type AssetId = T::AssetId; type Balance = T::Balance; fn balance( asset: Self::AssetId, who: &::AccountId, ) -> Self::Balance { Pallet::::balance(asset, who) } fn can_deposit( asset: Self::AssetId, who: &::AccountId, amount: Self::Balance, ) -> bool { Pallet::::can_deposit(asset, who, amount) } fn deposit( asset: Self::AssetId, who: ::AccountId, amount: Self::Balance, ) -> DispatchResult { Pallet::::increase_balance(asset, who, amount, None) } fn withdraw( asset: Self::AssetId, who: ::AccountId, amount: Self::Balance, ) -> DispatchResult { Pallet::::reduce_balance(asset, who, amount, None) } } type DepositBalanceOf = <::Currency as Currency<::AccountId>>::Balance; #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug)] pub struct AssetDetails< Balance, AccountId, DepositBalance, > { /// Can change `owner`, `issuer`, `freezer` and `admin` accounts. owner: AccountId, /// Can mint tokens. issuer: AccountId, /// Can thaw tokens, force transfers and burn tokens from any account. admin: AccountId, /// Can freeze tokens. freezer: AccountId, /// The total supply across all accounts. supply: Balance, /// The balance deposited for this asset. This pays for the data stored here. deposit: DepositBalance, /// The ED for virtual accounts. min_balance: Balance, /// If `true`, then any account with this asset is given a provider reference. Otherwise, it /// requires a consumer reference. is_sufficient: bool, /// The total number of accounts. accounts: u32, /// The total number of accounts for which we have placed a self-sufficient reference. sufficients: u32, /// The total number of approvals. approvals: u32, /// Whether the asset is frozen for non-admin transfers. is_frozen: bool, } impl AssetDetails { pub fn destroy_witness(&self) -> DestroyWitness { DestroyWitness { accounts: self.accounts, sufficients: self.sufficients, approvals: self.approvals, } } } /// A pair to act as a key for the approval storage map. #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug)] pub struct ApprovalKey { /// The owner of the funds that are being approved. owner: AccountId, /// The party to whom transfer of the funds is being delegated. delegate: AccountId, } /// Data concerning an approval. #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default)] pub struct Approval { /// The amount of funds approved for the balance transfer from the owner to some delegated /// target. amount: Balance, /// The amount reserved on the owner's account to hold this item in storage. deposit: DepositBalance, } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default)] pub struct AssetBalance { /// The balance. balance: Balance, /// Whether the account is frozen. is_frozen: bool, /// `true` if this balance gave the account a self-sufficient reference. sufficient: bool, } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default)] pub struct AssetMetadata { /// The balance deposited for this metadata. /// /// This pays for the data stored in this struct. deposit: DepositBalance, /// The user friendly name of this asset. Limited in length by `StringLimit`. name: Vec, /// The ticker symbol for this asset. Limited in length by `StringLimit`. symbol: Vec, /// The number of decimals this asset uses to represent one unit. decimals: u8, /// Whether the asset metadata may be changed by a non Force origin. is_frozen: bool, } /// Witness data for the destroy transactions. #[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug)] pub struct DestroyWitness { /// The number of accounts holding the asset. #[codec(compact)] accounts: u32, /// The number of accounts holding the asset with a self-sufficient reference. #[codec(compact)] sufficients: u32, /// The number of transfer-approvals of the asset. #[codec(compact)] approvals: u32, } #[frame_support::pallet] pub mod pallet { use frame_support::{ dispatch::DispatchResult, pallet_prelude::*, }; use frame_system::pallet_prelude::*; use super::*; #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(_); #[pallet::config] /// The module configuration trait. pub trait Config: frame_system::Config { /// The overarching event type. type Event: From> + IsType<::Event>; /// The units in which we record balances. type Balance: Member + Parameter + AtLeast32BitUnsigned + Default + Copy; /// Identifier for the class of asset. type AssetId: Member + Parameter + Default + Copy + HasCompact; /// The currency mechanism. type Currency: ReservableCurrency; /// The origin which may forcibly create or destroy an asset or otherwise alter privileged /// attributes. type ForceOrigin: EnsureOrigin; /// The basic amount of funds that must be reserved for an asset. type AssetDeposit: Get>; /// The basic amount of funds that must be reserved when adding metadata to your asset. type MetadataDepositBase: Get>; /// The additional funds that must be reserved for the number of bytes you store in your /// metadata. type MetadataDepositPerByte: Get>; /// The amount of funds that must be reserved when creating a new approval. type ApprovalDeposit: Get>; /// The maximum length of a name or symbol stored on-chain. type StringLimit: Get; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; } #[pallet::storage] /// Details of an asset. pub(super) type Asset = StorageMap< _, Blake2_128Concat, T::AssetId, AssetDetails>, >; #[pallet::storage] /// The number of units of assets held by any given account. pub(super) type Account = StorageDoubleMap< _, Blake2_128Concat, T::AssetId, Blake2_128Concat, T::AccountId, AssetBalance, ValueQuery, >; #[pallet::storage] /// Approved balance transfers. First balance is the amount approved for transfer. Second /// is the amount of `T::Currency` reserved for storing this. pub(super) type Approvals = StorageDoubleMap< _, Blake2_128Concat, T::AssetId, Blake2_128Concat, ApprovalKey, Approval>, OptionQuery, >; #[pallet::storage] /// Metadata of an asset. pub(super) type Metadata = StorageMap< _, Blake2_128Concat, T::AssetId, AssetMetadata>, ValueQuery, >; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] #[pallet::metadata(T::AccountId = "AccountId", T::Balance = "Balance", T::AssetId = "AssetId")] pub enum Event { /// Some asset class was created. \[asset_id, creator, owner\] Created(T::AssetId, T::AccountId, T::AccountId), /// Some assets were issued. \[asset_id, owner, total_supply\] Issued(T::AssetId, T::AccountId, T::Balance), /// Some assets were transferred. \[asset_id, from, to, amount\] Transferred(T::AssetId, T::AccountId, T::AccountId, T::Balance), /// Some assets were destroyed. \[asset_id, owner, balance\] Burned(T::AssetId, T::AccountId, T::Balance), /// The management team changed \[asset_id, issuer, admin, freezer\] TeamChanged(T::AssetId, T::AccountId, T::AccountId, T::AccountId), /// The owner changed \[asset_id, owner\] OwnerChanged(T::AssetId, T::AccountId), /// Some account `who` was frozen. \[asset_id, who\] Frozen(T::AssetId, T::AccountId), /// Some account `who` was thawed. \[asset_id, who\] Thawed(T::AssetId, T::AccountId), /// Some asset `asset_id` was frozen. \[asset_id\] AssetFrozen(T::AssetId), /// Some asset `asset_id` was thawed. \[asset_id\] AssetThawed(T::AssetId), /// An asset class was destroyed. Destroyed(T::AssetId), /// Some asset class was force-created. \[asset_id, owner\] ForceCreated(T::AssetId, T::AccountId), /// New metadata has been set for an asset. \[asset_id, name, symbol, decimals, is_frozen\] MetadataSet(T::AssetId, Vec, Vec, u8, bool), /// Metadata has been cleared for an asset. \[asset_id\] MetadataCleared(T::AssetId), /// (Additional) funds have been approved for transfer to a destination account. /// \[asset_id, source, delegate, amount\] ApprovedTransfer(T::AssetId, T::AccountId, T::AccountId, T::Balance), /// An approval for account `delegate` was cancelled by `owner`. /// \[id, owner, delegate\] ApprovalCancelled(T::AssetId, T::AccountId, T::AccountId), /// An `amount` was transferred in its entirety from `owner` to `destination` by /// the approved `delegate`. /// \[id, owner, delegate, destination\] TransferredApproved(T::AssetId, T::AccountId, T::AccountId, T::AccountId, T::Balance), /// An asset has had its attributes changed by the `Force` origin. /// \[id\] AssetStatusChanged(T::AssetId), } #[pallet::error] pub enum Error { /// Account balance must be greater than or equal to the transfer amount. BalanceLow, /// Balance should be non-zero. BalanceZero, /// The signing account has no permission to do the operation. NoPermission, /// The given asset ID is unknown. Unknown, /// The origin account is frozen. Frozen, /// The asset ID is already taken. InUse, /// Invalid witness data given. BadWitness, /// Minimum balance should be non-zero. MinBalanceZero, /// A mint operation lead to an overflow. Overflow, /// No provider reference exists to allow a non-zero balance of a non-self-sufficient asset. NoProvider, /// Invalid metadata given. BadMetadata, /// No approval exists that would allow the transfer. Unapproved, /// The source account would not survive the transfer and it needs to stay alive. WouldDie, } #[pallet::hooks] impl Hooks> for Pallet {} #[pallet::call] impl Pallet { /// Issue a new class of fungible assets from a public origin. /// /// This new asset class has no assets initially and its owner is the origin. /// /// The origin must be Signed and the sender must have sufficient funds free. /// /// Funds of sender are reserved by `AssetDeposit`. /// /// Parameters: /// - `id`: The identifier of the new asset. This must not be currently in use to identify /// an existing asset. /// - `admin`: The admin of this class of assets. The admin is the initial address of each /// member of the asset class's admin team. /// - `min_balance`: The minimum balance of this new asset that any single account must /// have. If an account's balance is reduced below this, then it collapses to zero. /// /// Emits `Created` event when successful. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::create())] pub(super) fn create( origin: OriginFor, #[pallet::compact] id: T::AssetId, admin: ::Source, min_balance: T::Balance, ) -> DispatchResult { let owner = ensure_signed(origin)?; let admin = T::Lookup::lookup(admin)?; ensure!(!Asset::::contains_key(id), Error::::InUse); ensure!(!min_balance.is_zero(), Error::::MinBalanceZero); let deposit = T::AssetDeposit::get(); T::Currency::reserve(&owner, deposit)?; Asset::::insert(id, AssetDetails { owner: owner.clone(), issuer: admin.clone(), admin: admin.clone(), freezer: admin.clone(), supply: Zero::zero(), deposit, min_balance, is_sufficient: false, accounts: 0, sufficients: 0, approvals: 0, is_frozen: false, }); Self::deposit_event(Event::Created(id, owner, admin)); Ok(()) } /// Issue a new class of fungible assets from a privileged origin. /// /// This new asset class has no assets initially. /// /// The origin must conform to `ForceOrigin`. /// /// Unlike `create`, no funds are reserved. /// /// - `id`: The identifier of the new asset. This must not be currently in use to identify /// an existing asset. /// - `owner`: The owner of this class of assets. The owner has full superuser permissions /// over this asset, but may later change and configure the permissions using `transfer_ownership` /// and `set_team`. /// - `max_zombies`: The total number of accounts which may hold assets in this class yet /// have no existential deposit. /// - `min_balance`: The minimum balance of this new asset that any single account must /// have. If an account's balance is reduced below this, then it collapses to zero. /// /// Emits `ForceCreated` event when successful. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::force_create())] pub(super) fn force_create( origin: OriginFor, #[pallet::compact] id: T::AssetId, owner: ::Source, is_sufficient: bool, #[pallet::compact] min_balance: T::Balance, ) -> DispatchResult { T::ForceOrigin::ensure_origin(origin)?; let owner = T::Lookup::lookup(owner)?; ensure!(!Asset::::contains_key(id), Error::::InUse); ensure!(!min_balance.is_zero(), Error::::MinBalanceZero); Asset::::insert(id, AssetDetails { owner: owner.clone(), issuer: owner.clone(), admin: owner.clone(), freezer: owner.clone(), supply: Zero::zero(), deposit: Zero::zero(), min_balance, is_sufficient, accounts: 0, sufficients: 0, approvals: 0, is_frozen: false, }); Self::deposit_event(Event::ForceCreated(id, owner)); Ok(()) } /// Destroy a class of fungible assets. /// /// The origin must conform to `ForceOrigin` or must be Signed and the sender must be the /// owner of the asset `id`. /// /// - `id`: The identifier of the asset to be destroyed. This must identify an existing /// asset. /// /// Emits `Destroyed` event when successful. /// /// Weight: `O(c + p + a)` where: /// - `c = (witness.accounts - witness.sufficients)` /// - `s = witness.sufficients` /// - `a = witness.approvals` #[pallet::weight(T::WeightInfo::destroy( witness.accounts.saturating_sub(witness.sufficients), witness.sufficients, witness.approvals, ))] pub(super) fn destroy( origin: OriginFor, #[pallet::compact] id: T::AssetId, witness: DestroyWitness, ) -> DispatchResult { let maybe_check_owner = match T::ForceOrigin::try_origin(origin) { Ok(_) => None, Err(origin) => Some(ensure_signed(origin)?), }; Asset::::try_mutate_exists(id, |maybe_details| { let mut details = maybe_details.take().ok_or(Error::::Unknown)?; if let Some(check_owner) = maybe_check_owner { ensure!(details.owner == check_owner, Error::::NoPermission); } ensure!(details.accounts == witness.accounts, Error::::BadWitness); ensure!(details.sufficients == witness.sufficients, Error::::BadWitness); ensure!(details.approvals == witness.approvals, Error::::BadWitness); for (who, v) in Account::::drain_prefix(id) { Self::dead_account(&who, &mut details, v.sufficient); } debug_assert_eq!(details.accounts, 0); debug_assert_eq!(details.sufficients, 0); let metadata = Metadata::::take(&id); T::Currency::unreserve(&details.owner, details.deposit.saturating_add(metadata.deposit)); Approvals::::remove_prefix(&id); Self::deposit_event(Event::Destroyed(id)); // NOTE: could use postinfo to reflect the actual number of accounts/sufficient/approvals Ok(()) }) } /// Mint assets of a particular class. /// /// The origin must be Signed and the sender must be the Issuer of the asset `id`. /// /// - `id`: The identifier of the asset to have some amount minted. /// - `beneficiary`: The account to be credited with the minted assets. /// - `amount`: The amount of the asset to be minted. /// /// Emits `Destroyed` event when successful. /// /// Weight: `O(1)` /// Modes: Pre-existing balance of `beneficiary`; Account pre-existence of `beneficiary`. #[pallet::weight(T::WeightInfo::mint())] pub(super) fn mint( origin: OriginFor, #[pallet::compact] id: T::AssetId, beneficiary: ::Source, #[pallet::compact] amount: T::Balance ) -> DispatchResult { let origin = ensure_signed(origin)?; let beneficiary = T::Lookup::lookup(beneficiary)?; Self::increase_balance(id, beneficiary, amount, Some(origin)) } /// Reduce the balance of `who` by as much as possible up to `amount` assets of `id`. /// /// Origin must be Signed and the sender should be the Manager of the asset `id`. /// /// Bails with `BalanceZero` if the `who` is already dead. /// /// - `id`: The identifier of the asset to have some amount burned. /// - `who`: The account to be debited from. /// - `amount`: The maximum amount by which `who`'s balance should be reduced. /// /// Emits `Burned` with the actual amount burned. If this takes the balance to below the /// minimum for the asset, then the amount burned is increased to take it to zero. /// /// Weight: `O(1)` /// Modes: Post-existence of `who`; Pre & post Zombie-status of `who`. #[pallet::weight(T::WeightInfo::burn())] pub(super) fn burn( origin: OriginFor, #[pallet::compact] id: T::AssetId, who: ::Source, #[pallet::compact] amount: T::Balance ) -> DispatchResult { let origin = ensure_signed(origin)?; let who = T::Lookup::lookup(who)?; Self::reduce_balance(id, who, amount, Some(origin)) } /// Move some assets from the sender account to another. /// /// Origin must be Signed. /// /// - `id`: The identifier of the asset to have some amount transferred. /// - `target`: The account to be credited. /// - `amount`: The amount by which the sender's balance of assets should be reduced and /// `target`'s balance increased. The amount actually transferred may be slightly greater in /// the case that the transfer would otherwise take the sender balance above zero but below /// the minimum balance. Must be greater than zero. /// /// Emits `Transferred` with the actual amount transferred. If this takes the source balance /// to below the minimum for the asset, then the amount transferred is increased to take it /// to zero. /// /// Weight: `O(1)` /// Modes: Pre-existence of `target`; Post-existence of sender; Prior & post zombie-status /// of sender; Account pre-existence of `target`. #[pallet::weight(T::WeightInfo::transfer())] pub(super) fn transfer( origin: OriginFor, #[pallet::compact] id: T::AssetId, target: ::Source, #[pallet::compact] amount: T::Balance ) -> DispatchResult { let origin = ensure_signed(origin)?; let dest = T::Lookup::lookup(target)?; Self::do_transfer(id, origin, dest, amount, None, false) } /// Move some assets from the sender account to another, keeping the sender account alive. /// /// Origin must be Signed. /// /// - `id`: The identifier of the asset to have some amount transferred. /// - `target`: The account to be credited. /// - `amount`: The amount by which the sender's balance of assets should be reduced and /// `target`'s balance increased. The amount actually transferred may be slightly greater in /// the case that the transfer would otherwise take the sender balance above zero but below /// the minimum balance. Must be greater than zero. /// /// Emits `Transferred` with the actual amount transferred. If this takes the source balance /// to below the minimum for the asset, then the amount transferred is increased to take it /// to zero. /// /// Weight: `O(1)` /// Modes: Pre-existence of `target`; Post-existence of sender; Prior & post zombie-status /// of sender; Account pre-existence of `target`. #[pallet::weight(T::WeightInfo::transfer_keep_alive())] pub(super) fn transfer_keep_alive( origin: OriginFor, #[pallet::compact] id: T::AssetId, target: ::Source, #[pallet::compact] amount: T::Balance ) -> DispatchResult { let origin = ensure_signed(origin)?; let dest = T::Lookup::lookup(target)?; Self::do_transfer(id, origin, dest, amount, None, true) } /// Move some assets from one account to another. /// /// Origin must be Signed and the sender should be the Admin of the asset `id`. /// /// - `id`: The identifier of the asset to have some amount transferred. /// - `source`: The account to be debited. /// - `dest`: The account to be credited. /// - `amount`: The amount by which the `source`'s balance of assets should be reduced and /// `dest`'s balance increased. The amount actually transferred may be slightly greater in /// the case that the transfer would otherwise take the `source` balance above zero but /// below the minimum balance. Must be greater than zero. /// /// Emits `Transferred` with the actual amount transferred. If this takes the source balance /// to below the minimum for the asset, then the amount transferred is increased to take it /// to zero. /// /// Weight: `O(1)` /// Modes: Pre-existence of `dest`; Post-existence of `source`; Prior & post zombie-status /// of `source`; Account pre-existence of `dest`. #[pallet::weight(T::WeightInfo::force_transfer())] pub(super) fn force_transfer( origin: OriginFor, #[pallet::compact] id: T::AssetId, source: ::Source, dest: ::Source, #[pallet::compact] amount: T::Balance, ) -> DispatchResult { let origin = ensure_signed(origin)?; let source = T::Lookup::lookup(source)?; let dest = T::Lookup::lookup(dest)?; Self::do_transfer(id, source, dest, amount, Some(origin), false) } /// Disallow further unprivileged transfers from an account. /// /// Origin must be Signed and the sender should be the Freezer of the asset `id`. /// /// - `id`: The identifier of the asset to be frozen. /// - `who`: The account to be frozen. /// /// Emits `Frozen`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::freeze())] pub(super) fn freeze( origin: OriginFor, #[pallet::compact] id: T::AssetId, who: ::Source ) -> DispatchResult { let origin = ensure_signed(origin)?; let d = Asset::::get(id).ok_or(Error::::Unknown)?; ensure!(&origin == &d.freezer, Error::::NoPermission); let who = T::Lookup::lookup(who)?; ensure!(Account::::contains_key(id, &who), Error::::BalanceZero); Account::::mutate(id, &who, |a| a.is_frozen = true); Self::deposit_event(Event::::Frozen(id, who)); Ok(()) } /// Allow unprivileged transfers from an account again. /// /// Origin must be Signed and the sender should be the Admin of the asset `id`. /// /// - `id`: The identifier of the asset to be frozen. /// - `who`: The account to be unfrozen. /// /// Emits `Thawed`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::thaw())] pub(super) fn thaw( origin: OriginFor, #[pallet::compact] id: T::AssetId, who: ::Source ) -> DispatchResult { let origin = ensure_signed(origin)?; let details = Asset::::get(id).ok_or(Error::::Unknown)?; ensure!(&origin == &details.admin, Error::::NoPermission); let who = T::Lookup::lookup(who)?; ensure!(Account::::contains_key(id, &who), Error::::BalanceZero); Account::::mutate(id, &who, |a| a.is_frozen = false); Self::deposit_event(Event::::Thawed(id, who)); Ok(()) } /// Disallow further unprivileged transfers for the asset class. /// /// Origin must be Signed and the sender should be the Freezer of the asset `id`. /// /// - `id`: The identifier of the asset to be frozen. /// /// Emits `Frozen`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::freeze_asset())] pub(super) fn freeze_asset( origin: OriginFor, #[pallet::compact] id: T::AssetId ) -> DispatchResult { let origin = ensure_signed(origin)?; Asset::::try_mutate(id, |maybe_details| { let d = maybe_details.as_mut().ok_or(Error::::Unknown)?; ensure!(&origin == &d.freezer, Error::::NoPermission); d.is_frozen = true; Self::deposit_event(Event::::AssetFrozen(id)); Ok(()) }) } /// Allow unprivileged transfers for the asset again. /// /// Origin must be Signed and the sender should be the Admin of the asset `id`. /// /// - `id`: The identifier of the asset to be frozen. /// /// Emits `Thawed`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::thaw_asset())] pub(super) fn thaw_asset( origin: OriginFor, #[pallet::compact] id: T::AssetId ) -> DispatchResult { let origin = ensure_signed(origin)?; Asset::::try_mutate(id, |maybe_details| { let d = maybe_details.as_mut().ok_or(Error::::Unknown)?; ensure!(&origin == &d.admin, Error::::NoPermission); d.is_frozen = false; Self::deposit_event(Event::::AssetThawed(id)); Ok(()) }) } /// Change the Owner of an asset. /// /// Origin must be Signed and the sender should be the Owner of the asset `id`. /// /// - `id`: The identifier of the asset. /// - `owner`: The new Owner of this asset. /// /// Emits `OwnerChanged`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::transfer_ownership())] pub(super) fn transfer_ownership( origin: OriginFor, #[pallet::compact] id: T::AssetId, owner: ::Source, ) -> DispatchResult { let origin = ensure_signed(origin)?; let owner = T::Lookup::lookup(owner)?; Asset::::try_mutate(id, |maybe_details| { let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; ensure!(&origin == &details.owner, Error::::NoPermission); if details.owner == owner { return Ok(()) } let metadata_deposit = Metadata::::get(id).deposit; let deposit = details.deposit + metadata_deposit; // Move the deposit to the new owner. T::Currency::repatriate_reserved(&details.owner, &owner, deposit, Reserved)?; details.owner = owner.clone(); Self::deposit_event(Event::OwnerChanged(id, owner)); Ok(()) }) } /// Change the Issuer, Admin and Freezer of an asset. /// /// Origin must be Signed and the sender should be the Owner of the asset `id`. /// /// - `id`: The identifier of the asset to be frozen. /// - `issuer`: The new Issuer of this asset. /// - `admin`: The new Admin of this asset. /// - `freezer`: The new Freezer of this asset. /// /// Emits `TeamChanged`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::set_team())] pub(super) fn set_team( origin: OriginFor, #[pallet::compact] id: T::AssetId, issuer: ::Source, admin: ::Source, freezer: ::Source, ) -> DispatchResult { let origin = ensure_signed(origin)?; let issuer = T::Lookup::lookup(issuer)?; let admin = T::Lookup::lookup(admin)?; let freezer = T::Lookup::lookup(freezer)?; Asset::::try_mutate(id, |maybe_details| { let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; ensure!(&origin == &details.owner, Error::::NoPermission); details.issuer = issuer.clone(); details.admin = admin.clone(); details.freezer = freezer.clone(); Self::deposit_event(Event::TeamChanged(id, issuer, admin, freezer)); Ok(()) }) } /// Set the metadata for an asset. /// /// Origin must be Signed and the sender should be the Owner of the asset `id`. /// /// Funds of sender are reserved according to the formula: /// `MetadataDepositBase + MetadataDepositPerByte * (name.len + symbol.len)` taking into /// account any already reserved funds. /// /// - `id`: The identifier of the asset to update. /// - `name`: The user friendly name of this asset. Limited in length by `StringLimit`. /// - `symbol`: The exchange symbol for this asset. Limited in length by `StringLimit`. /// - `decimals`: The number of decimals this asset uses to represent one unit. /// /// Emits `MetadataSet`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::set_metadata(name.len() as u32, symbol.len() as u32))] pub(super) fn set_metadata( origin: OriginFor, #[pallet::compact] id: T::AssetId, name: Vec, symbol: Vec, decimals: u8, ) -> DispatchResult { let origin = ensure_signed(origin)?; ensure!(name.len() <= T::StringLimit::get() as usize, Error::::BadMetadata); ensure!(symbol.len() <= T::StringLimit::get() as usize, Error::::BadMetadata); let d = Asset::::get(id).ok_or(Error::::Unknown)?; ensure!(&origin == &d.owner, Error::::NoPermission); Metadata::::try_mutate_exists(id, |metadata| { ensure!(metadata.as_ref().map_or(true, |m| !m.is_frozen), Error::::NoPermission); let old_deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit); let new_deposit = T::MetadataDepositPerByte::get() .saturating_mul(((name.len() + symbol.len()) as u32).into()) .saturating_add(T::MetadataDepositBase::get()); if new_deposit > old_deposit { T::Currency::reserve(&origin, new_deposit - old_deposit)?; } else { T::Currency::unreserve(&origin, old_deposit - new_deposit); } *metadata = Some(AssetMetadata { deposit: new_deposit, name: name.clone(), symbol: symbol.clone(), decimals, is_frozen: false, }); Self::deposit_event(Event::MetadataSet(id, name, symbol, decimals, false)); Ok(()) }) } /// Clear the metadata for an asset. /// /// Origin must be Signed and the sender should be the Owner of the asset `id`. /// /// Any deposit is freed for the asset owner. /// /// - `id`: The identifier of the asset to clear. /// /// Emits `MetadataCleared`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::clear_metadata())] pub(super) fn clear_metadata( origin: OriginFor, #[pallet::compact] id: T::AssetId, ) -> DispatchResult { let origin = ensure_signed(origin)?; let d = Asset::::get(id).ok_or(Error::::Unknown)?; ensure!(&origin == &d.owner, Error::::NoPermission); Metadata::::try_mutate_exists(id, |metadata| { let deposit = metadata.take().ok_or(Error::::Unknown)?.deposit; T::Currency::unreserve(&d.owner, deposit); Self::deposit_event(Event::MetadataCleared(id)); Ok(()) }) } /// Force the metadata for an asset to some value. /// /// Origin must be ForceOrigin. /// /// Any deposit is left alone. /// /// - `id`: The identifier of the asset to update. /// - `name`: The user friendly name of this asset. Limited in length by `StringLimit`. /// - `symbol`: The exchange symbol for this asset. Limited in length by `StringLimit`. /// - `decimals`: The number of decimals this asset uses to represent one unit. /// /// Emits `MetadataSet`. /// /// Weight: `O(N + S)` where N and S are the length of the name and symbol respectively. #[pallet::weight(T::WeightInfo::force_set_metadata(name.len() as u32, symbol.len() as u32))] pub(super) fn force_set_metadata( origin: OriginFor, #[pallet::compact] id: T::AssetId, name: Vec, symbol: Vec, decimals: u8, is_frozen: bool, ) -> DispatchResult { T::ForceOrigin::ensure_origin(origin)?; ensure!(name.len() <= T::StringLimit::get() as usize, Error::::BadMetadata); ensure!(symbol.len() <= T::StringLimit::get() as usize, Error::::BadMetadata); ensure!(Asset::::contains_key(id), Error::::Unknown); Metadata::::try_mutate_exists(id, |metadata| { let deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit); *metadata = Some(AssetMetadata { deposit, name: name.clone(), symbol: symbol.clone(), decimals, is_frozen, }); Self::deposit_event(Event::MetadataSet(id, name, symbol, decimals, is_frozen)); Ok(()) }) } /// Clear the metadata for an asset. /// /// Origin must be ForceOrigin. /// /// Any deposit is returned. /// /// - `id`: The identifier of the asset to clear. /// /// Emits `MetadataCleared`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::force_clear_metadata())] pub(super) fn force_clear_metadata( origin: OriginFor, #[pallet::compact] id: T::AssetId, ) -> DispatchResult { T::ForceOrigin::ensure_origin(origin)?; let d = Asset::::get(id).ok_or(Error::::Unknown)?; Metadata::::try_mutate_exists(id, |metadata| { let deposit = metadata.take().ok_or(Error::::Unknown)?.deposit; T::Currency::unreserve(&d.owner, deposit); Self::deposit_event(Event::MetadataCleared(id)); Ok(()) }) } /// Alter the attributes of a given asset. /// /// Origin must be `ForceOrigin`. /// /// - `id`: The identifier of the asset. /// - `owner`: The new Owner of this asset. /// - `issuer`: The new Issuer of this asset. /// - `admin`: The new Admin of this asset. /// - `freezer`: The new Freezer of this asset. /// - `min_balance`: The minimum balance of this new asset that any single account must /// have. If an account's balance is reduced below this, then it collapses to zero. /// - `is_sufficient`: Whether a non-zero balance of this asset is deposit of sufficient /// value to account for the state bloat associated with its balance storage. If set to /// `true`, then non-zero balances may be stored without a `consumer` reference (and thus /// an ED in the Balances pallet or whatever else is used to control user-account state /// growth). /// - `is_frozen`: Whether this asset class is frozen except for permissioned/admin /// instructions. /// /// Emits `AssetStatusChanged` with the identity of the asset. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::force_asset_status())] pub(super) fn force_asset_status( origin: OriginFor, #[pallet::compact] id: T::AssetId, owner: ::Source, issuer: ::Source, admin: ::Source, freezer: ::Source, #[pallet::compact] min_balance: T::Balance, is_sufficient: bool, is_frozen: bool, ) -> DispatchResult { T::ForceOrigin::ensure_origin(origin)?; Asset::::try_mutate(id, |maybe_asset| { let mut asset = maybe_asset.take().ok_or(Error::::Unknown)?; asset.owner = T::Lookup::lookup(owner)?; asset.issuer = T::Lookup::lookup(issuer)?; asset.admin = T::Lookup::lookup(admin)?; asset.freezer = T::Lookup::lookup(freezer)?; asset.min_balance = min_balance; asset.is_sufficient = is_sufficient; asset.is_frozen = is_frozen; *maybe_asset = Some(asset); Self::deposit_event(Event::AssetStatusChanged(id)); Ok(()) }) } /// Approve an amount of asset for transfer by a delegated third-party account. /// /// Origin must be Signed. /// /// Ensures that `ApprovalDeposit` worth of `Currency` is reserved from signing account /// for the purpose of holding the approval. If some non-zero amount of assets is already /// approved from signing account to `delegate`, then it is topped up or unreserved to /// meet the right value. /// /// NOTE: The signing account does not need to own `amount` of assets at the point of /// making this call. /// /// - `id`: The identifier of the asset. /// - `delegate`: The account to delegate permission to transfer asset. /// - `amount`: The amount of asset that may be transferred by `delegate`. If there is /// already an approval in place, then this acts additively. /// /// Emits `ApprovedTransfer` on success. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::approve_transfer())] pub(super) fn approve_transfer( origin: OriginFor, #[pallet::compact] id: T::AssetId, delegate: ::Source, #[pallet::compact] amount: T::Balance, ) -> DispatchResult { let owner = ensure_signed(origin)?; let delegate = T::Lookup::lookup(delegate)?; let key = ApprovalKey { owner, delegate }; Approvals::::try_mutate(id, &key, |maybe_approved| -> DispatchResult { let mut approved = maybe_approved.take().unwrap_or_default(); let deposit_required = T::ApprovalDeposit::get(); if approved.deposit < deposit_required { T::Currency::reserve(&key.owner, deposit_required - approved.deposit)?; approved.deposit = deposit_required; } approved.amount = approved.amount.saturating_add(amount); *maybe_approved = Some(approved); Ok(()) })?; Self::deposit_event(Event::ApprovedTransfer(id, key.owner, key.delegate, amount)); Ok(()) } /// Cancel all of some asset approved for delegated transfer by a third-party account. /// /// Origin must be Signed and there must be an approval in place between signer and /// `delegate`. /// /// Unreserves any deposit previously reserved by `approve_transfer` for the approval. /// /// - `id`: The identifier of the asset. /// - `delegate`: The account delegated permission to transfer asset. /// /// Emits `ApprovalCancelled` on success. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::cancel_approval())] pub(super) fn cancel_approval( origin: OriginFor, #[pallet::compact] id: T::AssetId, delegate: ::Source, ) -> DispatchResult { let owner = ensure_signed(origin)?; let delegate = T::Lookup::lookup(delegate)?; let key = ApprovalKey { owner, delegate }; let approval = Approvals::::take(id, &key).ok_or(Error::::Unknown)?; T::Currency::unreserve(&key.owner, approval.deposit); Self::deposit_event(Event::ApprovalCancelled(id, key.owner, key.delegate)); Ok(()) } /// Cancel all of some asset approved for delegated transfer by a third-party account. /// /// Origin must be either ForceOrigin or Signed origin with the signer being the Admin /// account of the asset `id`. /// /// Unreserves any deposit previously reserved by `approve_transfer` for the approval. /// /// - `id`: The identifier of the asset. /// - `delegate`: The account delegated permission to transfer asset. /// /// Emits `ApprovalCancelled` on success. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::force_cancel_approval())] pub(super) fn force_cancel_approval( origin: OriginFor, #[pallet::compact] id: T::AssetId, owner: ::Source, delegate: ::Source, ) -> DispatchResult { T::ForceOrigin::try_origin(origin) .map(|_| ()) .or_else(|origin| -> DispatchResult { let origin = ensure_signed(origin)?; let d = Asset::::get(id).ok_or(Error::::Unknown)?; ensure!(&origin == &d.admin, Error::::NoPermission); Ok(()) })?; let owner = T::Lookup::lookup(owner)?; let delegate = T::Lookup::lookup(delegate)?; let key = ApprovalKey { owner, delegate }; let approval = Approvals::::take(id, &key).ok_or(Error::::Unknown)?; T::Currency::unreserve(&key.owner, approval.deposit); Self::deposit_event(Event::ApprovalCancelled(id, key.owner, key.delegate)); Ok(()) } /// Transfer some asset balance from a previously delegated account to some third-party /// account. /// /// Origin must be Signed and there must be an approval in place by the `owner` to the /// signer. /// /// If the entire amount approved for transfer is transferred, then any deposit previously /// reserved by `approve_transfer` is unreserved. /// /// - `id`: The identifier of the asset. /// - `owner`: The account which previously approved for a transfer of at least `amount` and /// from which the asset balance will be withdrawn. /// - `destination`: The account to which the asset balance of `amount` will be transferred. /// - `amount`: The amount of assets to transfer. /// /// Emits `TransferredApproved` on success. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::transfer_approved())] pub(super) fn transfer_approved( origin: OriginFor, #[pallet::compact] id: T::AssetId, owner: ::Source, destination: ::Source, #[pallet::compact] amount: T::Balance, ) -> DispatchResult { let delegate = ensure_signed(origin)?; let owner = T::Lookup::lookup(owner)?; let destination = T::Lookup::lookup(destination)?; let key = ApprovalKey { owner, delegate }; Approvals::::try_mutate_exists(id, &key, |maybe_approved| -> DispatchResult { let mut approved = maybe_approved.take().ok_or(Error::::Unapproved)?; let remaining = approved.amount.checked_sub(&amount).ok_or(Error::::Unapproved)?; Self::do_transfer(id, key.owner.clone(), destination, amount, None, false)?; if remaining.is_zero() { T::Currency::unreserve(&key.owner, approved.deposit); } else { approved.amount = remaining; *maybe_approved = Some(approved); } Ok(()) })?; Ok(()) } } } // The main implementation block for the module. impl Pallet { // Public immutables /// Get the asset `id` balance of `who`. pub fn balance(id: T::AssetId, who: impl sp_std::borrow::Borrow) -> T::Balance { Account::::get(id, who.borrow()).balance } /// Get the total supply of an asset `id`. pub fn total_supply(id: T::AssetId) -> T::Balance { Asset::::get(id).map(|x| x.supply).unwrap_or_else(Zero::zero) } fn new_account( who: &T::AccountId, d: &mut AssetDetails>, ) -> Result { let accounts = d.accounts.checked_add(1).ok_or(Error::::Overflow)?; let is_sufficient = if d.is_sufficient { frame_system::Pallet::::inc_sufficients(who); d.sufficients += 1; true } else { frame_system::Pallet::::inc_consumers(who).map_err(|_| Error::::NoProvider)?; false }; d.accounts = accounts; Ok(is_sufficient) } fn dead_account( who: &T::AccountId, d: &mut AssetDetails>, sufficient: bool, ) { if sufficient { d.sufficients = d.sufficients.saturating_sub(1); frame_system::Pallet::::dec_sufficients(who); } else { frame_system::Pallet::::dec_consumers(who); } d.accounts = d.accounts.saturating_sub(1); } fn can_deposit(id: T::AssetId, who: &T::AccountId, amount: T::Balance) -> bool { let details = match Asset::::get(id) { Some(details) => details, None => return false, }; if details.supply.checked_add(&amount).is_none() { return false } let account = Account::::get(id, who); if account.balance.checked_add(&amount).is_none() { return false } if account.balance.is_zero() { if amount < details.min_balance { return false } if !details.is_sufficient && frame_system::Pallet::::providers(who) == 0 { return false } if details.is_sufficient && details.sufficients.checked_add(1).is_none() { return false } } true } fn increase_balance( id: T::AssetId, beneficiary: T::AccountId, amount: T::Balance, maybe_check_issuer: Option, ) -> DispatchResult { Asset::::try_mutate(id, |maybe_details| { let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; if let Some(check_issuer) = maybe_check_issuer { ensure!(&check_issuer == &details.issuer, Error::::NoPermission); } details.supply = details.supply.checked_add(&amount).ok_or(Error::::Overflow)?; Account::::try_mutate(id, &beneficiary, |t| -> DispatchResult { let new_balance = t.balance.saturating_add(amount); ensure!(new_balance >= details.min_balance, Error::::BalanceLow); if t.balance.is_zero() { t.sufficient = Self::new_account(&beneficiary, details)?; } t.balance = new_balance; Ok(()) })?; Self::deposit_event(Event::Issued(id, beneficiary, amount)); Ok(()) }) } fn reduce_balance( id: T::AssetId, target: T::AccountId, amount: T::Balance, maybe_check_admin: Option, ) -> DispatchResult { Asset::::try_mutate(id, |maybe_details| { let d = maybe_details.as_mut().ok_or(Error::::Unknown)?; if let Some(check_admin) = maybe_check_admin { ensure!(&check_admin == &d.admin, Error::::NoPermission); } let burned = Account::::try_mutate_exists( id, &target, |maybe_account| -> Result { let mut account = maybe_account.take().ok_or(Error::::BalanceZero)?; let mut burned = amount.min(account.balance); account.balance -= burned; *maybe_account = if account.balance < d.min_balance { burned += account.balance; Self::dead_account(&target, d, account.sufficient); None } else { Some(account) }; Ok(burned) } )?; d.supply = d.supply.saturating_sub(burned); Self::deposit_event(Event::Burned(id, target, burned)); Ok(()) }) } fn do_transfer( id: T::AssetId, source: T::AccountId, dest: T::AccountId, amount: T::Balance, maybe_need_admin: Option, keep_alive: bool, ) -> DispatchResult { let mut source_account = Account::::get(id, &source); ensure!(!source_account.is_frozen, Error::::Frozen); source_account.balance = source_account.balance.checked_sub(&amount) .ok_or(Error::::BalanceLow)?; Asset::::try_mutate(id, |maybe_details| { let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; ensure!(!details.is_frozen, Error::::Frozen); if let Some(need_admin) = maybe_need_admin { ensure!(&need_admin == &details.admin, Error::::NoPermission); } if dest != source && !amount.is_zero() { let mut amount = amount; if source_account.balance < details.min_balance { ensure!(!keep_alive, Error::::WouldDie); amount += source_account.balance; source_account.balance = Zero::zero(); } Account::::try_mutate(id, &dest, |a| -> DispatchResult { let new_balance = a.balance.saturating_add(amount); ensure!(new_balance >= details.min_balance, Error::::BalanceLow); if a.balance.is_zero() { a.sufficient = Self::new_account(&dest, details)?; } a.balance = new_balance; Ok(()) })?; if source_account.balance.is_zero() { Self::dead_account(&source, details, source_account.sufficient); Account::::remove(id, &source); } else { Account::::insert(id, &source, &source_account) } } Self::deposit_event(Event::Transferred(id, source, dest, amount)); Ok(()) }) } }