#![recursion_limit = "1024"]
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "runtime-benchmarks")]
pub mod benchmarking;
pub mod migration;
#[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::*;
extern crate alloc;
extern crate core;
use scale_info::TypeInfo;
use sp_runtime::{
traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, Saturating, StaticLookup, Zero},
ArithmeticError, DispatchError, TokenError,
};
use alloc::vec::Vec;
use core::{fmt::Debug, marker::PhantomData};
use frame_support::{
dispatch::DispatchResult,
ensure,
pallet_prelude::DispatchResultWithPostInfo,
storage::KeyPrefixIterator,
traits::{
tokens::{
fungibles, DepositConsequence, Fortitude,
Preservation::{Expendable, Preserve},
WithdrawConsequence,
},
BalanceStatus::Reserved,
Currency, EnsureOriginWithArg, Incrementable, ReservableCurrency, StoredMap,
},
};
use frame_system::Config as SystemConfig;
pub use pallet::*;
pub use weights::WeightInfo;
type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
const LOG_TARGET: &str = "runtime::assets";
pub trait AssetsCallback<AssetId, AccountId> {
fn created(_id: &AssetId, _owner: &AccountId) -> Result<(), ()> {
Ok(())
}
fn destroyed(_id: &AssetId) -> Result<(), ()> {
Ok(())
}
}
#[impl_trait_for_tuples::impl_for_tuples(10)]
impl<AssetId, AccountId> AssetsCallback<AssetId, AccountId> for Tuple {
fn created(id: &AssetId, owner: &AccountId) -> Result<(), ()> {
for_tuples!( #( Tuple::created(id, owner)?; )* );
Ok(())
}
fn destroyed(id: &AssetId) -> Result<(), ()> {
for_tuples!( #( Tuple::destroyed(id)?; )* );
Ok(())
}
}
pub struct AutoIncAssetId<T, I = ()>(PhantomData<(T, I)>);
impl<T: Config<I>, I> AssetsCallback<T::AssetId, T::AccountId> for AutoIncAssetId<T, I>
where
T::AssetId: Incrementable,
{
fn created(_: &T::AssetId, _: &T::AccountId) -> Result<(), ()> {
let Some(next_id) = NextAssetId::<T, I>::get() else {
return Ok(());
};
let next_id = next_id.increment().ok_or(())?;
NextAssetId::<T, I>::put(next_id);
Ok(())
}
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
use codec::HasCompact;
use frame_support::{
pallet_prelude::*,
traits::{tokens::ProvideAssetReserves, AccountTouch, ContainsPair},
};
use frame_system::pallet_prelude::*;
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
pub const MAX_RESERVES: u32 = 5;
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T, I = ()>(_);
#[cfg(feature = "runtime-benchmarks")]
pub trait BenchmarkHelper<AssetIdParameter, ReserveIdParameter> {
fn create_asset_id_parameter(id: u32) -> AssetIdParameter;
fn create_reserve_id_parameter(id: u32) -> ReserveIdParameter;
}
#[cfg(feature = "runtime-benchmarks")]
impl<AssetIdParameter: From<u32>> BenchmarkHelper<AssetIdParameter, ()> for () {
fn create_asset_id_parameter(id: u32) -> AssetIdParameter {
id.into()
}
fn create_reserve_id_parameter(_: u32) -> () {
()
}
}
pub mod config_preludes {
use super::*;
use frame_support::derive_impl;
pub struct TestDefaultConfig;
#[derive_impl(frame_system::config_preludes::TestDefaultConfig, no_aggregated_types)]
impl frame_system::DefaultConfig for TestDefaultConfig {}
#[frame_support::register_default_impl(TestDefaultConfig)]
impl DefaultConfig for TestDefaultConfig {
#[inject_runtime_type]
type RuntimeEvent = ();
type Balance = u64;
type RemoveItemsLimit = ConstU32<5>;
type AssetId = u32;
type AssetIdParameter = u32;
type ReserveData = ();
type AssetDeposit = ConstUint<1>;
type AssetAccountDeposit = ConstUint<10>;
type MetadataDepositBase = ConstUint<1>;
type MetadataDepositPerByte = ConstUint<1>;
type ApprovalDeposit = ConstUint<1>;
type StringLimit = ConstU32<50>;
type Freezer = ();
type Holder = ();
type Extra = ();
type CallbackHandle = ();
type WeightInfo = ();
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();
}
}
#[pallet::config(with_default)]
pub trait Config<I: 'static = ()>: frame_system::Config {
#[pallet::no_default_bounds]
#[allow(deprecated)]
type RuntimeEvent: From<Event<Self, I>>
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
type Balance: Member
+ Parameter
+ HasCompact<Type: DecodeWithMemTracking>
+ AtLeast32BitUnsigned
+ Default
+ Copy
+ MaybeSerializeDeserialize
+ MaxEncodedLen
+ TypeInfo;
#[pallet::constant]
type RemoveItemsLimit: Get<u32>;
type AssetId: Member + Parameter + Clone + MaybeSerializeDeserialize + MaxEncodedLen;
type AssetIdParameter: Parameter + From<Self::AssetId> + Into<Self::AssetId> + MaxEncodedLen;
type ReserveData: Debug + Parameter + MaybeSerializeDeserialize + MaxEncodedLen;
#[pallet::no_default]
type Currency: ReservableCurrency<Self::AccountId>;
#[pallet::no_default]
type CreateOrigin: EnsureOriginWithArg<
Self::RuntimeOrigin,
Self::AssetId,
Success = Self::AccountId,
>;
#[pallet::no_default]
type ForceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
#[pallet::constant]
#[pallet::no_default_bounds]
type AssetDeposit: Get<DepositBalanceOf<Self, I>>;
#[pallet::constant]
#[pallet::no_default_bounds]
type AssetAccountDeposit: Get<DepositBalanceOf<Self, I>>;
#[pallet::constant]
#[pallet::no_default_bounds]
type MetadataDepositBase: Get<DepositBalanceOf<Self, I>>;
#[pallet::constant]
#[pallet::no_default_bounds]
type MetadataDepositPerByte: Get<DepositBalanceOf<Self, I>>;
#[pallet::constant]
#[pallet::no_default_bounds]
type ApprovalDeposit: Get<DepositBalanceOf<Self, I>>;
#[pallet::constant]
type StringLimit: Get<u32>;
type Freezer: FrozenBalance<Self::AssetId, Self::AccountId, Self::Balance>;
type Holder: BalanceOnHold<Self::AssetId, Self::AccountId, Self::Balance>;
type Extra: Member + Parameter + Default + MaxEncodedLen;
type CallbackHandle: AssetsCallback<Self::AssetId, Self::AccountId>;
type WeightInfo: WeightInfo;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper: BenchmarkHelper<Self::AssetIdParameter, Self::ReserveData>;
}
#[pallet::storage]
pub type Asset<T: Config<I>, I: 'static = ()> = StorageMap<
_,
Blake2_128Concat,
T::AssetId,
AssetDetails<T::Balance, T::AccountId, DepositBalanceOf<T, I>>,
>;
#[pallet::storage]
pub type Account<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
_,
Blake2_128Concat,
T::AssetId,
Blake2_128Concat,
T::AccountId,
AssetAccountOf<T, I>,
>;
#[pallet::storage]
pub type Approvals<T: Config<I>, I: 'static = ()> = StorageNMap<
_,
(
NMapKey<Blake2_128Concat, T::AssetId>,
NMapKey<Blake2_128Concat, T::AccountId>, // owner
NMapKey<Blake2_128Concat, T::AccountId>, // delegate
),
Approval<T::Balance, DepositBalanceOf<T, I>>,
>;
#[pallet::storage]
pub type Metadata<T: Config<I>, I: 'static = ()> = StorageMap<
_,
Blake2_128Concat,
T::AssetId,
AssetMetadata<DepositBalanceOf<T, I>, BoundedVec<u8, T::StringLimit>>,
ValueQuery,
>;
#[pallet::storage]
pub type Reserves<T: Config<I>, I: 'static = ()> = StorageMap<
_,
Blake2_128Concat,
T::AssetId,
BoundedVec<T::ReserveData, ConstU32<MAX_RESERVES>>,
ValueQuery,
>;
#[pallet::storage]
pub type NextAssetId<T: Config<I>, I: 'static = ()> = StorageValue<_, T::AssetId, OptionQuery>;
#[pallet::genesis_config]
#[derive(frame_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
pub assets: Vec<(T::AssetId, T::AccountId, bool, T::Balance)>,
pub metadata: Vec<(T::AssetId, Vec<u8>, Vec<u8>, u8)>,
pub accounts: Vec<(T::AssetId, T::AccountId, T::Balance)>,
pub next_asset_id: Option<T::AssetId>,
pub reserves: Vec<(T::AssetId, Vec<T::ReserveData>)>,
}
#[pallet::genesis_build]
impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
fn build(&self) {
for (id, owner, is_sufficient, min_balance) in &self.assets {
assert!(!Asset::<T, I>::contains_key(id), "Asset id already in use");
assert!(!min_balance.is_zero(), "Min balance should not be zero");
Asset::<T, I>::insert(
id,
AssetDetails {
owner: owner.clone(),
issuer: owner.clone(),
admin: owner.clone(),
freezer: owner.clone(),
supply: Zero::zero(),
deposit: Zero::zero(),
min_balance: *min_balance,
is_sufficient: *is_sufficient,
accounts: 0,
sufficients: 0,
approvals: 0,
status: AssetStatus::Live,
},
);
}
for (id, name, symbol, decimals) in &self.metadata {
assert!(Asset::<T, I>::contains_key(id), "Asset does not exist");
let bounded_name: BoundedVec<u8, T::StringLimit> =
name.clone().try_into().expect("asset name is too long");
let bounded_symbol: BoundedVec<u8, T::StringLimit> =
symbol.clone().try_into().expect("asset symbol is too long");
let metadata = AssetMetadata {
deposit: Zero::zero(),
name: bounded_name,
symbol: bounded_symbol,
decimals: *decimals,
is_frozen: false,
};
Metadata::<T, I>::insert(id, metadata);
}
for (id, account_id, amount) in &self.accounts {
let result = <Pallet<T, I>>::increase_balance(
id.clone(),
account_id,
*amount,
|details| -> DispatchResult {
debug_assert!(
details.supply.checked_add(&amount).is_some(),
"checked in prep; qed"
);
details.supply = details.supply.saturating_add(*amount);
Ok(())
},
);
assert!(result.is_ok());
}
if let Some(next_asset_id) = &self.next_asset_id {
NextAssetId::<T, I>::put(next_asset_id);
}
for (id, reserves) in &self.reserves {
assert!(!Reserves::<T, I>::contains_key(id), "Asset id already in use");
let reserves = BoundedVec::try_from(reserves.clone()).expect("too many reserves");
Reserves::<T, I>::insert(id, reserves);
}
}
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config<I>, I: 'static = ()> {
Created { asset_id: T::AssetId, creator: T::AccountId, owner: T::AccountId },
Issued { asset_id: T::AssetId, owner: T::AccountId, amount: T::Balance },
Transferred {
asset_id: T::AssetId,
from: T::AccountId,
to: T::AccountId,
amount: T::Balance,
},
Burned { asset_id: T::AssetId, owner: T::AccountId, balance: T::Balance },
TeamChanged {
asset_id: T::AssetId,
issuer: T::AccountId,
admin: T::AccountId,
freezer: T::AccountId,
},
OwnerChanged { asset_id: T::AssetId, owner: T::AccountId },
Frozen { asset_id: T::AssetId, who: T::AccountId },
Thawed { asset_id: T::AssetId, who: T::AccountId },
AssetFrozen { asset_id: T::AssetId },
AssetThawed { asset_id: T::AssetId },
AccountsDestroyed { asset_id: T::AssetId, accounts_destroyed: u32, accounts_remaining: u32 },
ApprovalsDestroyed {
asset_id: T::AssetId,
approvals_destroyed: u32,
approvals_remaining: u32,
},
DestructionStarted { asset_id: T::AssetId },
Destroyed { asset_id: T::AssetId },
ForceCreated { asset_id: T::AssetId, owner: T::AccountId },
MetadataSet {
asset_id: T::AssetId,
name: Vec<u8>,
symbol: Vec<u8>,
decimals: u8,
is_frozen: bool,
},
MetadataCleared { asset_id: T::AssetId },
ApprovedTransfer {
asset_id: T::AssetId,
source: T::AccountId,
delegate: T::AccountId,
amount: T::Balance,
},
ApprovalCancelled { asset_id: T::AssetId, owner: T::AccountId, delegate: T::AccountId },
TransferredApproved {
asset_id: T::AssetId,
owner: T::AccountId,
delegate: T::AccountId,
destination: T::AccountId,
amount: T::Balance,
},
AssetStatusChanged { asset_id: T::AssetId },
AssetMinBalanceChanged { asset_id: T::AssetId, new_min_balance: T::Balance },
Touched { asset_id: T::AssetId, who: T::AccountId, depositor: T::AccountId },
Blocked { asset_id: T::AssetId, who: T::AccountId },
Deposited { asset_id: T::AssetId, who: T::AccountId, amount: T::Balance },
Withdrawn { asset_id: T::AssetId, who: T::AccountId, amount: T::Balance },
ReservesUpdated { asset_id: T::AssetId, reserves: Vec<T::ReserveData> },
ReservesRemoved { asset_id: T::AssetId },
IssuedCredit { asset_id: T::AssetId, amount: T::Balance },
BurnedCredit { asset_id: T::AssetId, amount: T::Balance },
IssuedDebt { asset_id: T::AssetId, amount: T::Balance },
BurnedDebt { asset_id: T::AssetId, amount: T::Balance },
}
#[pallet::error]
pub enum Error<T, I = ()> {
BalanceLow,
NoAccount,
NoPermission,
Unknown,
Frozen,
InUse,
BadWitness,
MinBalanceZero,
UnavailableConsumer,
BadMetadata,
Unapproved,
WouldDie,
AlreadyExists,
NoDeposit,
WouldBurn,
LiveAsset,
AssetNotLive,
IncorrectStatus,
NotFrozen,
CallbackFailed,
BadAssetId,
ContainsFreezes,
ContainsHolds,
TooManyReserves,
}
#[pallet::hooks]
impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
#[cfg(feature = "try-runtime")]
fn try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
Self::do_try_state()
}
}
#[pallet::call(weight(<T as Config<I>>::WeightInfo))]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::call_index(0)]
pub fn create(
origin: OriginFor<T>,
id: T::AssetIdParameter,
admin: AccountIdLookupOf<T>,
min_balance: T::Balance,
) -> DispatchResult {
let id: T::AssetId = id.into();
let owner = T::CreateOrigin::ensure_origin(origin, &id)?;
let admin = T::Lookup::lookup(admin)?;
ensure!(!Asset::<T, I>::contains_key(&id), Error::<T, I>::InUse);
ensure!(!min_balance.is_zero(), Error::<T, I>::MinBalanceZero);
if let Some(next_id) = NextAssetId::<T, I>::get() {
ensure!(id == next_id, Error::<T, I>::BadAssetId);
}
let deposit = T::AssetDeposit::get();
T::Currency::reserve(&owner, deposit)?;
Asset::<T, I>::insert(
id.clone(),
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,
status: AssetStatus::Live,
},
);
ensure!(T::CallbackHandle::created(&id, &owner).is_ok(), Error::<T, I>::CallbackFailed);
Self::deposit_event(Event::Created {
asset_id: id,
creator: owner.clone(),
owner: admin,
});
Ok(())
}
#[pallet::call_index(1)]
pub fn force_create(
origin: OriginFor<T>,
id: T::AssetIdParameter,
owner: AccountIdLookupOf<T>,
is_sufficient: bool,
#[pallet::compact] min_balance: T::Balance,
) -> DispatchResult {
T::ForceOrigin::ensure_origin(origin)?;
let owner = T::Lookup::lookup(owner)?;
let id: T::AssetId = id.into();
Self::do_force_create(id, owner, is_sufficient, min_balance)
}
#[pallet::call_index(2)]
pub fn start_destroy(origin: OriginFor<T>, id: T::AssetIdParameter) -> DispatchResult {
let maybe_check_owner = match T::ForceOrigin::try_origin(origin) {
Ok(_) => None,
Err(origin) => Some(ensure_signed(origin)?),
};
let id: T::AssetId = id.into();
Self::do_start_destroy(id, maybe_check_owner)
}
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::destroy_accounts(T::RemoveItemsLimit::get()))]
pub fn destroy_accounts(
origin: OriginFor<T>,
id: T::AssetIdParameter,
) -> DispatchResultWithPostInfo {
ensure_signed(origin)?;
let id: T::AssetId = id.into();
let removed_accounts = Self::do_destroy_accounts(id, T::RemoveItemsLimit::get())?;
Ok(Some(T::WeightInfo::destroy_accounts(removed_accounts)).into())
}
#[pallet::call_index(4)]
#[pallet::weight(T::WeightInfo::destroy_approvals(T::RemoveItemsLimit::get()))]
pub fn destroy_approvals(
origin: OriginFor<T>,
id: T::AssetIdParameter,
) -> DispatchResultWithPostInfo {
ensure_signed(origin)?;
let id: T::AssetId = id.into();
let removed_approvals = Self::do_destroy_approvals(id, T::RemoveItemsLimit::get())?;
Ok(Some(T::WeightInfo::destroy_approvals(removed_approvals)).into())
}
#[pallet::call_index(5)]
pub fn finish_destroy(origin: OriginFor<T>, id: T::AssetIdParameter) -> DispatchResult {
ensure_signed(origin)?;
let id: T::AssetId = id.into();
Self::do_finish_destroy(id)
}
#[pallet::call_index(6)]
pub fn mint(
origin: OriginFor<T>,
id: T::AssetIdParameter,
beneficiary: AccountIdLookupOf<T>,
#[pallet::compact] amount: T::Balance,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let beneficiary = T::Lookup::lookup(beneficiary)?;
let id: T::AssetId = id.into();
Self::do_mint(id, &beneficiary, amount, Some(origin))?;
Ok(())
}
#[pallet::call_index(7)]
pub fn burn(
origin: OriginFor<T>,
id: T::AssetIdParameter,
who: AccountIdLookupOf<T>,
#[pallet::compact] amount: T::Balance,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let who = T::Lookup::lookup(who)?;
let id: T::AssetId = id.into();
let f = DebitFlags { keep_alive: false, best_effort: true };
Self::do_burn(id, &who, amount, Some(origin), f)?;
Ok(())
}
#[pallet::call_index(8)]
pub fn transfer(
origin: OriginFor<T>,
id: T::AssetIdParameter,
target: AccountIdLookupOf<T>,
#[pallet::compact] amount: T::Balance,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let dest = T::Lookup::lookup(target)?;
let id: T::AssetId = id.into();
let f = TransferFlags { keep_alive: false, best_effort: false, burn_dust: false };
Self::do_transfer(id, &origin, &dest, amount, None, f).map(|_| ())
}
#[pallet::call_index(9)]
pub fn transfer_keep_alive(
origin: OriginFor<T>,
id: T::AssetIdParameter,
target: AccountIdLookupOf<T>,
#[pallet::compact] amount: T::Balance,
) -> DispatchResult {
let source = ensure_signed(origin)?;
let dest = T::Lookup::lookup(target)?;
let id: T::AssetId = id.into();
let f = TransferFlags { keep_alive: true, best_effort: false, burn_dust: false };
Self::do_transfer(id, &source, &dest, amount, None, f).map(|_| ())
}
#[pallet::call_index(10)]
pub fn force_transfer(
origin: OriginFor<T>,
id: T::AssetIdParameter,
source: AccountIdLookupOf<T>,
dest: AccountIdLookupOf<T>,
#[pallet::compact] amount: T::Balance,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let source = T::Lookup::lookup(source)?;
let dest = T::Lookup::lookup(dest)?;
let id: T::AssetId = id.into();
let f = TransferFlags { keep_alive: false, best_effort: false, burn_dust: false };
Self::do_transfer(id, &source, &dest, amount, Some(origin), f).map(|_| ())
}
#[pallet::call_index(11)]
pub fn freeze(
origin: OriginFor<T>,
id: T::AssetIdParameter,
who: AccountIdLookupOf<T>,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let id: T::AssetId = id.into();
let d = Asset::<T, I>::get(&id).ok_or(Error::<T, I>::Unknown)?;
ensure!(
d.status == AssetStatus::Live || d.status == AssetStatus::Frozen,
Error::<T, I>::IncorrectStatus
);
ensure!(origin == d.freezer, Error::<T, I>::NoPermission);
let who = T::Lookup::lookup(who)?;
Account::<T, I>::try_mutate(&id, &who, |maybe_account| -> DispatchResult {
maybe_account.as_mut().ok_or(Error::<T, I>::NoAccount)?.status =
AccountStatus::Frozen;
Ok(())
})?;
Self::deposit_event(Event::<T, I>::Frozen { asset_id: id, who });
Ok(())
}
#[pallet::call_index(12)]
pub fn thaw(
origin: OriginFor<T>,
id: T::AssetIdParameter,
who: AccountIdLookupOf<T>,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let id: T::AssetId = id.into();
let details = Asset::<T, I>::get(&id).ok_or(Error::<T, I>::Unknown)?;
ensure!(
details.status == AssetStatus::Live || details.status == AssetStatus::Frozen,
Error::<T, I>::IncorrectStatus
);
ensure!(origin == details.admin, Error::<T, I>::NoPermission);
let who = T::Lookup::lookup(who)?;
Account::<T, I>::try_mutate(&id, &who, |maybe_account| -> DispatchResult {
maybe_account.as_mut().ok_or(Error::<T, I>::NoAccount)?.status =
AccountStatus::Liquid;
Ok(())
})?;
Self::deposit_event(Event::<T, I>::Thawed { asset_id: id, who });
Ok(())
}
#[pallet::call_index(13)]
pub fn freeze_asset(origin: OriginFor<T>, id: T::AssetIdParameter) -> DispatchResult {
let origin = ensure_signed(origin)?;
let id: T::AssetId = id.into();
Asset::<T, I>::try_mutate(id.clone(), |maybe_details| {
let d = maybe_details.as_mut().ok_or(Error::<T, I>::Unknown)?;
ensure!(d.status == AssetStatus::Live, Error::<T, I>::AssetNotLive);
ensure!(origin == d.freezer, Error::<T, I>::NoPermission);
d.status = AssetStatus::Frozen;
Self::deposit_event(Event::<T, I>::AssetFrozen { asset_id: id });
Ok(())
})
}
#[pallet::call_index(14)]
pub fn thaw_asset(origin: OriginFor<T>, id: T::AssetIdParameter) -> DispatchResult {
let origin = ensure_signed(origin)?;
let id: T::AssetId = id.into();
Asset::<T, I>::try_mutate(id.clone(), |maybe_details| {
let d = maybe_details.as_mut().ok_or(Error::<T, I>::Unknown)?;
ensure!(origin == d.admin, Error::<T, I>::NoPermission);
ensure!(d.status == AssetStatus::Frozen, Error::<T, I>::NotFrozen);
d.status = AssetStatus::Live;
Self::deposit_event(Event::<T, I>::AssetThawed { asset_id: id });
Ok(())
})
}
#[pallet::call_index(15)]
pub fn transfer_ownership(
origin: OriginFor<T>,
id: T::AssetIdParameter,
owner: AccountIdLookupOf<T>,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let owner = T::Lookup::lookup(owner)?;
let id: T::AssetId = id.into();
Asset::<T, I>::try_mutate(id.clone(), |maybe_details| {
let details = maybe_details.as_mut().ok_or(Error::<T, I>::Unknown)?;
ensure!(details.status == AssetStatus::Live, Error::<T, I>::AssetNotLive);
ensure!(origin == details.owner, Error::<T, I>::NoPermission);
if details.owner == owner {
return Ok(());
}
let metadata_deposit = Metadata::<T, I>::get(&id).deposit;
let deposit = details.deposit + metadata_deposit;
T::Currency::repatriate_reserved(&details.owner, &owner, deposit, Reserved)?;
details.owner = owner.clone();
Self::deposit_event(Event::OwnerChanged { asset_id: id, owner });
Ok(())
})
}
#[pallet::call_index(16)]
pub fn set_team(
origin: OriginFor<T>,
id: T::AssetIdParameter,
issuer: AccountIdLookupOf<T>,
admin: AccountIdLookupOf<T>,
freezer: AccountIdLookupOf<T>,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let issuer = T::Lookup::lookup(issuer)?;
let admin = T::Lookup::lookup(admin)?;
let freezer = T::Lookup::lookup(freezer)?;
let id: T::AssetId = id.into();
Asset::<T, I>::try_mutate(id.clone(), |maybe_details| {
let details = maybe_details.as_mut().ok_or(Error::<T, I>::Unknown)?;
ensure!(details.status == AssetStatus::Live, Error::<T, I>::AssetNotLive);
ensure!(origin == details.owner, Error::<T, I>::NoPermission);
details.issuer = issuer.clone();
details.admin = admin.clone();
details.freezer = freezer.clone();
Self::deposit_event(Event::TeamChanged { asset_id: id, issuer, admin, freezer });
Ok(())
})
}
#[pallet::call_index(17)]
#[pallet::weight(T::WeightInfo::set_metadata(name.len() as u32, symbol.len() as u32))]
pub fn set_metadata(
origin: OriginFor<T>,
id: T::AssetIdParameter,
name: Vec<u8>,
symbol: Vec<u8>,
decimals: u8,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let id: T::AssetId = id.into();
Self::do_set_metadata(id, &origin, name, symbol, decimals)
}
#[pallet::call_index(18)]
pub fn clear_metadata(origin: OriginFor<T>, id: T::AssetIdParameter) -> DispatchResult {
let origin = ensure_signed(origin)?;
let id: T::AssetId = id.into();
let d = Asset::<T, I>::get(&id).ok_or(Error::<T, I>::Unknown)?;
ensure!(d.status == AssetStatus::Live, Error::<T, I>::AssetNotLive);
ensure!(origin == d.owner, Error::<T, I>::NoPermission);
Metadata::<T, I>::try_mutate_exists(id.clone(), |metadata| {
let deposit = metadata.take().ok_or(Error::<T, I>::Unknown)?.deposit;
T::Currency::unreserve(&d.owner, deposit);
Self::deposit_event(Event::MetadataCleared { asset_id: id });
Ok(())
})
}
#[pallet::call_index(19)]
#[pallet::weight(T::WeightInfo::force_set_metadata(name.len() as u32, symbol.len() as u32))]
pub fn force_set_metadata(
origin: OriginFor<T>,
id: T::AssetIdParameter,
name: Vec<u8>,
symbol: Vec<u8>,
decimals: u8,
is_frozen: bool,
) -> DispatchResult {
T::ForceOrigin::ensure_origin(origin)?;
let id: T::AssetId = id.into();
let bounded_name: BoundedVec<u8, T::StringLimit> =
name.clone().try_into().map_err(|_| Error::<T, I>::BadMetadata)?;
let bounded_symbol: BoundedVec<u8, T::StringLimit> =
symbol.clone().try_into().map_err(|_| Error::<T, I>::BadMetadata)?;
ensure!(Asset::<T, I>::contains_key(&id), Error::<T, I>::Unknown);
Metadata::<T, I>::try_mutate_exists(id.clone(), |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 {
asset_id: id,
name,
symbol,
decimals,
is_frozen,
});
Ok(())
})
}
#[pallet::call_index(20)]
pub fn force_clear_metadata(
origin: OriginFor<T>,
id: T::AssetIdParameter,
) -> DispatchResult {
T::ForceOrigin::ensure_origin(origin)?;
let id: T::AssetId = id.into();
let d = Asset::<T, I>::get(&id).ok_or(Error::<T, I>::Unknown)?;
Metadata::<T, I>::try_mutate_exists(id.clone(), |metadata| {
let deposit = metadata.take().ok_or(Error::<T, I>::Unknown)?.deposit;
T::Currency::unreserve(&d.owner, deposit);
Self::deposit_event(Event::MetadataCleared { asset_id: id });
Ok(())
})
}
#[pallet::call_index(21)]
pub fn force_asset_status(
origin: OriginFor<T>,
id: T::AssetIdParameter,
owner: AccountIdLookupOf<T>,
issuer: AccountIdLookupOf<T>,
admin: AccountIdLookupOf<T>,
freezer: AccountIdLookupOf<T>,
#[pallet::compact] min_balance: T::Balance,
is_sufficient: bool,
is_frozen: bool,
) -> DispatchResult {
T::ForceOrigin::ensure_origin(origin)?;
let id: T::AssetId = id.into();
Asset::<T, I>::try_mutate(id.clone(), |maybe_asset| {
let mut asset = maybe_asset.take().ok_or(Error::<T, I>::Unknown)?;
ensure!(asset.status != AssetStatus::Destroying, Error::<T, I>::AssetNotLive);
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;
if is_frozen {
asset.status = AssetStatus::Frozen;
} else {
asset.status = AssetStatus::Live;
}
*maybe_asset = Some(asset);
Self::deposit_event(Event::AssetStatusChanged { asset_id: id });
Ok(())
})
}
#[pallet::call_index(22)]
pub fn approve_transfer(
origin: OriginFor<T>,
id: T::AssetIdParameter,
delegate: AccountIdLookupOf<T>,
#[pallet::compact] amount: T::Balance,
) -> DispatchResult {
let owner = ensure_signed(origin)?;
let delegate = T::Lookup::lookup(delegate)?;
let id: T::AssetId = id.into();
Self::do_approve_transfer(id, &owner, &delegate, amount)
}
#[pallet::call_index(23)]
pub fn cancel_approval(
origin: OriginFor<T>,
id: T::AssetIdParameter,
delegate: AccountIdLookupOf<T>,
) -> DispatchResult {
let owner = ensure_signed(origin)?;
let delegate = T::Lookup::lookup(delegate)?;
let id: T::AssetId = id.into();
let mut d = Asset::<T, I>::get(&id).ok_or(Error::<T, I>::Unknown)?;
ensure!(d.status == AssetStatus::Live, Error::<T, I>::AssetNotLive);
let approval = Approvals::<T, I>::take((id.clone(), &owner, &delegate))
.ok_or(Error::<T, I>::Unknown)?;
T::Currency::unreserve(&owner, approval.deposit);
d.approvals.saturating_dec();
Asset::<T, I>::insert(id.clone(), d);
Self::deposit_event(Event::ApprovalCancelled { asset_id: id, owner, delegate });
Ok(())
}
#[pallet::call_index(24)]
pub fn force_cancel_approval(
origin: OriginFor<T>,
id: T::AssetIdParameter,
owner: AccountIdLookupOf<T>,
delegate: AccountIdLookupOf<T>,
) -> DispatchResult {
let id: T::AssetId = id.into();
let mut d = Asset::<T, I>::get(&id).ok_or(Error::<T, I>::Unknown)?;
ensure!(d.status == AssetStatus::Live, Error::<T, I>::AssetNotLive);
T::ForceOrigin::try_origin(origin)
.map(|_| ())
.or_else(|origin| -> DispatchResult {
let origin = ensure_signed(origin)?;
ensure!(origin == d.admin, Error::<T, I>::NoPermission);
Ok(())
})?;
let owner = T::Lookup::lookup(owner)?;
let delegate = T::Lookup::lookup(delegate)?;
let approval = Approvals::<T, I>::take((id.clone(), &owner, &delegate))
.ok_or(Error::<T, I>::Unknown)?;
T::Currency::unreserve(&owner, approval.deposit);
d.approvals.saturating_dec();
Asset::<T, I>::insert(id.clone(), d);
Self::deposit_event(Event::ApprovalCancelled { asset_id: id, owner, delegate });
Ok(())
}
#[pallet::call_index(25)]
pub fn transfer_approved(
origin: OriginFor<T>,
id: T::AssetIdParameter,
owner: AccountIdLookupOf<T>,
destination: AccountIdLookupOf<T>,
#[pallet::compact] amount: T::Balance,
) -> DispatchResult {
let delegate = ensure_signed(origin)?;
let owner = T::Lookup::lookup(owner)?;
let destination = T::Lookup::lookup(destination)?;
let id: T::AssetId = id.into();
Self::do_transfer_approved(id, &owner, &delegate, &destination, amount)
}
#[pallet::call_index(26)]
#[pallet::weight(T::WeightInfo::touch())]
pub fn touch(origin: OriginFor<T>, id: T::AssetIdParameter) -> DispatchResult {
let who = ensure_signed(origin)?;
let id: T::AssetId = id.into();
Self::do_touch(id, who.clone(), who)
}
#[pallet::call_index(27)]
#[pallet::weight(T::WeightInfo::refund())]
pub fn refund(
origin: OriginFor<T>,
id: T::AssetIdParameter,
allow_burn: bool,
) -> DispatchResult {
let id: T::AssetId = id.into();
Self::do_refund(id, ensure_signed(origin)?, allow_burn)
}
#[pallet::call_index(28)]
pub fn set_min_balance(
origin: OriginFor<T>,
id: T::AssetIdParameter,
min_balance: T::Balance,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let id: T::AssetId = id.into();
let mut details = Asset::<T, I>::get(&id).ok_or(Error::<T, I>::Unknown)?;
ensure!(origin == details.owner, Error::<T, I>::NoPermission);
let old_min_balance = details.min_balance;
ensure!(!details.is_sufficient, Error::<T, I>::NoPermission);
ensure!(
min_balance < old_min_balance || details.accounts == 0,
Error::<T, I>::NoPermission
);
details.min_balance = min_balance;
Asset::<T, I>::insert(&id, details);
Self::deposit_event(Event::AssetMinBalanceChanged {
asset_id: id,
new_min_balance: min_balance,
});
Ok(())
}
#[pallet::call_index(29)]
#[pallet::weight(T::WeightInfo::touch_other())]
pub fn touch_other(
origin: OriginFor<T>,
id: T::AssetIdParameter,
who: AccountIdLookupOf<T>,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let who = T::Lookup::lookup(who)?;
let id: T::AssetId = id.into();
Self::do_touch(id, who, origin)
}
#[pallet::call_index(30)]
#[pallet::weight(T::WeightInfo::refund_other())]
pub fn refund_other(
origin: OriginFor<T>,
id: T::AssetIdParameter,
who: AccountIdLookupOf<T>,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let who = T::Lookup::lookup(who)?;
let id: T::AssetId = id.into();
Self::do_refund_other(id, &who, Some(origin))
}
#[pallet::call_index(31)]
pub fn block(
origin: OriginFor<T>,
id: T::AssetIdParameter,
who: AccountIdLookupOf<T>,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let id: T::AssetId = id.into();
let d = Asset::<T, I>::get(&id).ok_or(Error::<T, I>::Unknown)?;
ensure!(
d.status == AssetStatus::Live || d.status == AssetStatus::Frozen,
Error::<T, I>::IncorrectStatus
);
ensure!(origin == d.freezer, Error::<T, I>::NoPermission);
let who = T::Lookup::lookup(who)?;
Account::<T, I>::try_mutate(&id, &who, |maybe_account| -> DispatchResult {
maybe_account.as_mut().ok_or(Error::<T, I>::NoAccount)?.status =
AccountStatus::Blocked;
Ok(())
})?;
Self::deposit_event(Event::<T, I>::Blocked { asset_id: id, who });
Ok(())
}
#[pallet::call_index(32)]
#[pallet::weight(T::WeightInfo::transfer_all())]
pub fn transfer_all(
origin: OriginFor<T>,
id: T::AssetIdParameter,
dest: AccountIdLookupOf<T>,
keep_alive: bool,
) -> DispatchResult {
let transactor = ensure_signed(origin)?;
let keep_alive = if keep_alive { Preserve } else { Expendable };
let reducible_balance = <Self as fungibles::Inspect<_>>::reducible_balance(
id.clone().into(),
&transactor,
keep_alive,
Fortitude::Polite,
);
let dest = T::Lookup::lookup(dest)?;
<Self as fungibles::Mutate<_>>::transfer(
id.into(),
&transactor,
&dest,
reducible_balance,
keep_alive,
)?;
Ok(())
}
#[pallet::call_index(33)]
#[pallet::weight(T::WeightInfo::set_reserves(reserves.len() as u32))]
pub fn set_reserves(
origin: OriginFor<T>,
id: T::AssetIdParameter,
reserves: BoundedVec<T::ReserveData, ConstU32<MAX_RESERVES>>,
) -> DispatchResult {
let id: T::AssetId = id.into();
let origin = ensure_signed(origin.clone())
.or_else(|_| T::CreateOrigin::ensure_origin(origin, &id))?;
let details = Asset::<T, I>::get(&id).ok_or(Error::<T, I>::Unknown)?;
ensure!(origin == details.owner, Error::<T, I>::NoPermission);
Self::unchecked_update_reserves(id, reserves)?;
Ok(())
}
}
#[pallet::view_functions]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
pub fn asset_details(
id: T::AssetId,
) -> Option<AssetDetails<T::Balance, T::AccountId, DepositBalanceOf<T, I>>> {
Asset::<T, I>::get(id)
}
pub fn balance_of(who: T::AccountId, id: T::AssetId) -> Option<<T as Config<I>>::Balance> {
Account::<T, I>::get(id, who).map(|account| account.balance)
}
pub fn get_metadata(
id: T::AssetId,
) -> Option<AssetMetadata<DepositBalanceOf<T, I>, BoundedVec<u8, T::StringLimit>>> {
Metadata::<T, I>::try_get(id).ok()
}
pub fn get_reserves_data(id: T::AssetId) -> Vec<T::ReserveData> {
Self::reserves(&id)
}
}
impl<T: Config<I>, I: 'static> AccountTouch<T::AssetId, T::AccountId> for Pallet<T, I> {
type Balance = DepositBalanceOf<T, I>;
fn deposit_required(_: T::AssetId) -> Self::Balance {
T::AssetAccountDeposit::get()
}
fn should_touch(asset: T::AssetId, who: &T::AccountId) -> bool {
match Asset::<T, I>::get(&asset) {
Some(info) if info.is_sufficient => false,
Some(_) if frame_system::Pallet::<T>::can_accrue_consumers(who, 2) => false,
Some(_) => !Account::<T, I>::contains_key(asset, who),
_ => true,
}
}
fn touch(
asset: T::AssetId,
who: &T::AccountId,
depositor: &T::AccountId,
) -> DispatchResult {
Self::do_touch(asset, who.clone(), depositor.clone())
}
}
impl<T: Config<I>, I: 'static> ContainsPair<T::AssetId, T::AccountId> for Pallet<T, I> {
fn contains(asset: &T::AssetId, who: &T::AccountId) -> bool {
Account::<T, I>::contains_key(asset, who)
}
}
impl<T: Config<I>, I: 'static> ProvideAssetReserves<T::AssetId, T::ReserveData> for Pallet<T, I> {
fn reserves(id: &T::AssetId) -> Vec<T::ReserveData> {
Reserves::<T, I>::get(id).into_inner()
}
}
}
#[cfg(any(feature = "try-runtime", test))]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
for asset_id in Reserves::<T, I>::iter_keys() {
ensure!(Asset::<T, I>::contains_key(asset_id.clone()), "Orphaned Reserves data found");
}
for asset_id in Metadata::<T, I>::iter_keys() {
ensure!(Asset::<T, I>::contains_key(asset_id.clone()), "Orphaned Metadata found");
}
for (asset_id, _, _) in Approvals::<T, I>::iter_keys() {
ensure!(Asset::<T, I>::contains_key(asset_id.clone()), "Orphaned Approval found");
}
for (asset_id, _) in Account::<T, I>::iter_keys() {
ensure!(Asset::<T, I>::contains_key(asset_id.clone()), "Orphaned Account found");
}
for (asset_id, details) in Asset::<T, I>::iter() {
if details.status == AssetStatus::Destroying {
continue;
}
let mut calculated_supply = T::Balance::zero();
let mut calculated_accounts = 0u32;
let mut calculated_sufficients = 0u32;
for (who, account) in Account::<T, I>::iter_prefix(&asset_id) {
let held = T::Holder::balance_on_hold(asset_id.clone(), &who).unwrap_or_default();
calculated_supply =
calculated_supply.saturating_add(account.balance).saturating_add(held);
calculated_accounts += 1;
if matches!(account.reason, ExistenceReason::Sufficient) {
calculated_sufficients += 1;
}
if account.balance < details.min_balance {
ensure!(
matches!(
account.reason,
ExistenceReason::DepositHeld(_) | ExistenceReason::DepositFrom(_, _)
),
"Account below min_balance must have a deposit"
);
}
}
ensure!(details.supply >= calculated_supply, "Asset supply mismatch");
ensure!(details.accounts == calculated_accounts, "Asset account count mismatch");
ensure!(
details.sufficients == calculated_sufficients,
"Asset sufficients count mismatch"
);
let calculated_approvals = Approvals::<T, I>::iter_prefix((&asset_id,)).count() as u32;
if details.approvals != calculated_approvals {
log::error!(
"Asset {asset_id:?} approvals count mismatch: calculated {calculated_approvals} vs expected {}",
details.approvals,
);
return Err("Asset approvals count mismatch".into());
}
}
Ok(())
}
}
sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $);