// 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 Pallet //! //! 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 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 [`Pallet`] 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)] #[cfg(feature = "runtime-benchmarks")] mod benchmarking; #[cfg(test)] pub mod mock; #[cfg(test)] mod tests; pub mod weights; mod extra_mutator; pub use extra_mutator::*; mod functions; mod impl_fungibles; mod impl_stored_map; mod types; pub use types::*; use codec::HasCompact; use frame_support::{ dispatch::{DispatchError, DispatchResult}, ensure, traits::{ tokens::{fungibles, DepositConsequence, WithdrawConsequence}, BalanceStatus::Reserved, Currency, ReservableCurrency, StoredMap, }, }; use frame_system::Config as SystemConfig; use sp_runtime::{ traits::{ AtLeast32BitUnsigned, Bounded, CheckedAdd, CheckedSub, Saturating, StaticLookup, Zero, }, ArithmeticError, TokenError, }; use sp_std::{borrow::Borrow, convert::TryInto, prelude::*}; pub use pallet::*; pub use weights::WeightInfo; #[frame_support::pallet] pub mod pallet { use super::*; use frame_support::{dispatch::DispatchResult, pallet_prelude::*}; use frame_system::pallet_prelude::*; #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] #[pallet::generate_storage_info] 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 + MaxEncodedLen; /// Identifier for the class of asset. type AssetId: Member + Parameter + Default + Copy + HasCompact + MaxEncodedLen; /// 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. #[pallet::constant] type AssetDeposit: Get>; /// The basic amount of funds that must be reserved when adding metadata to your asset. #[pallet::constant] type MetadataDepositBase: Get>; /// The additional funds that must be reserved for the number of bytes you store in your /// metadata. #[pallet::constant] type MetadataDepositPerByte: Get>; /// The amount of funds that must be reserved when creating a new approval. #[pallet::constant] type ApprovalDeposit: Get>; /// The maximum length of a name or symbol stored on-chain. #[pallet::constant] type StringLimit: Get; /// A hook to allow a per-asset, per-account minimum balance to be enforced. This must be /// respected in all permissionless operations. type Freezer: FrozenBalance; /// Additional data to be stored with an account's asset balance. type Extra: Member + Parameter + Default + MaxEncodedLen; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; } #[pallet::storage] /// Details of an asset. pub(super) type Asset, I: 'static = ()> = StorageMap< _, Blake2_128Concat, T::AssetId, AssetDetails>, >; #[pallet::storage] /// The number of units of assets held by any given account. pub(super) type Account, I: 'static = ()> = StorageDoubleMap< _, Blake2_128Concat, T::AssetId, Blake2_128Concat, T::AccountId, AssetBalance, ValueQuery, GetDefault, ConstU32<300_000>, >; #[pallet::storage] /// Approved balance transfers. First balance is the amount approved for transfer. Second /// is the amount of `T::Currency` reserved for storing this. /// First key is the asset ID, second key is the owner and third key is the delegate. pub(super) type Approvals, I: 'static = ()> = StorageNMap< _, ( NMapKey, NMapKey, // owner NMapKey, // delegate ), Approval>, OptionQuery, GetDefault, ConstU32<300_000>, >; #[pallet::storage] /// Metadata of an asset. pub(super) type Metadata, I: 'static = ()> = StorageMap< _, Blake2_128Concat, T::AssetId, AssetMetadata, BoundedVec>, ValueQuery, GetDefault, ConstU32<300_000>, >; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event, I: 'static = ()> { /// 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, /// 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::call] impl, I: 'static> 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 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`. /// - `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 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. /// /// NOTE: It can be helpful to first freeze an asset before destroying it so that you /// can provide accurate witness information and prevent users from manipulating state /// in a way that can make it harder to destroy. /// /// 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 fn destroy( origin: OriginFor, #[pallet::compact] id: T::AssetId, witness: DestroyWitness, ) -> DispatchResultWithPostInfo { 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(id, &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), ); for ((owner, _), approval) in Approvals::::drain_prefix((&id,)) { T::Currency::unreserve(&owner, approval.deposit); } Self::deposit_event(Event::Destroyed(id)); Ok(Some(T::WeightInfo::destroy( details.accounts.saturating_sub(details.sufficients), details.sufficients, details.approvals, )) .into()) }) } /// 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 `Issued` event when successful. /// /// Weight: `O(1)` /// Modes: Pre-existing balance of `beneficiary`; Account pre-existence of `beneficiary`. #[pallet::weight(T::WeightInfo::mint())] pub 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::do_mint(id, &beneficiary, amount, Some(origin))?; Ok(()) } /// 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 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)?; let f = DebitFlags { keep_alive: false, best_effort: true }; let _ = Self::do_burn(id, &who, amount, Some(origin), f)?; Ok(()) } /// 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; Account pre-existence of /// `target`. #[pallet::weight(T::WeightInfo::transfer())] pub 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)?; let f = TransferFlags { keep_alive: false, best_effort: false, burn_dust: false }; Self::do_transfer(id, &origin, &dest, amount, None, f).map(|_| ()) } /// 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; Account pre-existence of /// `target`. #[pallet::weight(T::WeightInfo::transfer_keep_alive())] pub fn transfer_keep_alive( origin: OriginFor, #[pallet::compact] id: T::AssetId, target: ::Source, #[pallet::compact] amount: T::Balance, ) -> DispatchResult { let source = ensure_signed(origin)?; let dest = T::Lookup::lookup(target)?; let f = TransferFlags { keep_alive: true, best_effort: false, burn_dust: false }; Self::do_transfer(id, &source, &dest, amount, None, f).map(|_| ()) } /// 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`; Account pre-existence of /// `dest`. #[pallet::weight(T::WeightInfo::force_transfer())] pub 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)?; let f = TransferFlags { keep_alive: false, best_effort: false, burn_dust: false }; Self::do_transfer(id, &source, &dest, amount, Some(origin), f).map(|_| ()) } /// 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 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 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 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 thawed. /// /// Emits `Thawed`. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::thaw_asset())] pub 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 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 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 fn set_metadata( origin: OriginFor, #[pallet::compact] id: T::AssetId, name: Vec, symbol: Vec, decimals: u8, ) -> DispatchResult { let origin = ensure_signed(origin)?; let bounded_name: BoundedVec = name.clone().try_into().map_err(|_| Error::::BadMetadata)?; let bounded_symbol: BoundedVec = symbol.clone().try_into().map_err(|_| 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: bounded_name, symbol: bounded_symbol, 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 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 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)?; let bounded_name: BoundedVec = name.clone().try_into().map_err(|_| Error::::BadMetadata)?; let bounded_symbol: BoundedVec = symbol.clone().try_into().map_err(|_| 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: bounded_name, symbol: bounded_symbol, 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 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 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 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 mut d = Asset::::get(id).ok_or(Error::::Unknown)?; ensure!(!d.is_frozen, Error::::Frozen); Approvals::::try_mutate( (id, &owner, &delegate), |maybe_approved| -> DispatchResult { let mut approved = match maybe_approved.take() { // an approval already exists and is being updated Some(a) => a, // a new approval is created None => { d.approvals.saturating_inc(); Default::default() }, }; let deposit_required = T::ApprovalDeposit::get(); if approved.deposit < deposit_required { T::Currency::reserve(&owner, deposit_required - approved.deposit)?; approved.deposit = deposit_required; } approved.amount = approved.amount.saturating_add(amount); *maybe_approved = Some(approved); Ok(()) }, )?; Asset::::insert(id, d); Self::deposit_event(Event::ApprovedTransfer(id, owner, 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 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 mut d = Asset::::get(id).ok_or(Error::::Unknown)?; let approval = Approvals::::take((id, &owner, &delegate)).ok_or(Error::::Unknown)?; T::Currency::unreserve(&owner, approval.deposit); d.approvals.saturating_dec(); Asset::::insert(id, d); Self::deposit_event(Event::ApprovalCancelled(id, owner, 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 fn force_cancel_approval( origin: OriginFor, #[pallet::compact] id: T::AssetId, owner: ::Source, delegate: ::Source, ) -> DispatchResult { let mut d = Asset::::get(id).ok_or(Error::::Unknown)?; T::ForceOrigin::try_origin(origin) .map(|_| ()) .or_else(|origin| -> DispatchResult { let origin = ensure_signed(origin)?; ensure!(&origin == &d.admin, Error::::NoPermission); Ok(()) })?; let owner = T::Lookup::lookup(owner)?; let delegate = T::Lookup::lookup(delegate)?; let approval = Approvals::::take((id, &owner, &delegate)).ok_or(Error::::Unknown)?; T::Currency::unreserve(&owner, approval.deposit); d.approvals.saturating_dec(); Asset::::insert(id, d); Self::deposit_event(Event::ApprovalCancelled(id, owner, 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 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)?; Approvals::::try_mutate_exists( (id, &owner, delegate), |maybe_approved| -> DispatchResult { let mut approved = maybe_approved.take().ok_or(Error::::Unapproved)?; let remaining = approved.amount.checked_sub(&amount).ok_or(Error::::Unapproved)?; let f = TransferFlags { keep_alive: false, best_effort: false, burn_dust: false }; Self::do_transfer(id, &owner, &destination, amount, None, f)?; if remaining.is_zero() { T::Currency::unreserve(&owner, approved.deposit); Asset::::mutate(id, |maybe_details| { if let Some(details) = maybe_details { details.approvals.saturating_dec(); } }); } else { approved.amount = remaining; *maybe_approved = Some(approved); } Ok(()) }, )?; Ok(()) } } }