Implement fungible::* for Balances (#8454)

* Reservable, Transferrable Fungible(s), plus adapters.

* Repot into new dir

* Imbalances for Fungibles

* Repot and balanced fungible.

* Clean up names and bridge-over Imbalanced.

* Repot frame_support::trait. Finally.

* Make build.

* Docs

* Good errors

* Fix tests. Implement fungible::Inspect for Balances.

* Implement additional traits for Balances.

* Revert UI test "fixes"

* Fix UI error

* Fix UI test

* More work on fungibles

* Fixes

* More work.

* Update lock

* Make fungible::reserved work for Balances

* Introduce Freezer to Assets, ready for a reserve & locks pallet. Some renaming/refactoring.

* Cleanup errors

* Imbalances working with Assets

* Test for freezer.

* Grumbles

* Grumbles

* Fixes

* Extra "side-car" data for a user's asset balance.

* Fix

* Fix test

* Fixes

* Line lengths

* Comments

* Update frame/assets/src/tests.rs

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>

* Update frame/support/src/traits/tokens/fungibles.rs

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>

* Update frame/assets/src/lib.rs

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>

* Update frame/support/src/traits/tokens/fungible.rs

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>

* Introduce `transfer_reserved`

* Rename fungible Reserve -> Hold, add flag structs

* Avoid the `melted` API - its too complex and gives little help

* Repot Assets pallet

Co-authored-by: Bastian Köcher <info@kchr.de>
Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>
This commit is contained in:
Gavin Wood
2021-03-28 20:59:34 +02:00
committed by GitHub
parent c2dd5e21a4
commit d0eee4f1cb
17 changed files with 1537 additions and 554 deletions
+16
View File
@@ -4635,6 +4635,22 @@ dependencies = [
"sp-std",
]
[[package]]
name = "pallet-assets-freezer"
version = "3.0.0"
dependencies = [
"frame-benchmarking",
"frame-support",
"frame-system",
"pallet-assets",
"parity-scale-codec 2.0.1",
"serde",
"sp-core",
"sp-io",
"sp-runtime",
"sp-std",
]
[[package]]
name = "pallet-atomic-swap"
version = "3.0.0"
+2
View File
@@ -1045,6 +1045,8 @@ impl pallet_assets::Config for Runtime {
type MetadataDepositPerByte = MetadataDepositPerByte;
type ApprovalDeposit = ApprovalDeposit;
type StringLimit = StringLimit;
type Freezer = ();
type Extra = ();
type WeightInfo = pallet_assets::weights::SubstrateWeight<Runtime>;
}
+105
View File
@@ -0,0 +1,105 @@
// 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.
//! Datatype for easy mutation of the extra "sidecar" data.
use super::*;
/// A mutator type allowing inspection and possible modification of the extra "sidecar" data.
///
/// This may be used as a `Deref` for the pallet's extra data. If mutated (using `DerefMut`), then
/// any uncommitted changes (see `commit` function) will be automatically committed to storage when
/// dropped. Changes, even after committed, may be reverted to their original values with the
/// `revert` function.
pub struct ExtraMutator<T: Config> {
id: T::AssetId,
who: T::AccountId,
original: T::Extra,
pending: Option<T::Extra>,
}
impl<T: Config> Drop for ExtraMutator<T> {
fn drop(&mut self) {
debug_assert!(self.commit().is_ok(), "attempt to write to non-existent asset account");
}
}
impl<T: Config> sp_std::ops::Deref for ExtraMutator<T> {
type Target = T::Extra;
fn deref(&self) -> &T::Extra {
match self.pending {
Some(ref value) => value,
None => &self.original,
}
}
}
impl<T: Config> sp_std::ops::DerefMut for ExtraMutator<T> {
fn deref_mut(&mut self) -> &mut T::Extra {
if self.pending.is_none() {
self.pending = Some(self.original.clone());
}
self.pending.as_mut().unwrap()
}
}
impl<T: Config> ExtraMutator<T> {
pub(super) fn maybe_new(id: T::AssetId, who: impl sp_std::borrow::Borrow<T::AccountId>)
-> Option<ExtraMutator<T>>
{
if Account::<T>::contains_key(id, who.borrow()) {
Some(ExtraMutator::<T> {
id,
who: who.borrow().clone(),
original: Account::<T>::get(id, who.borrow()).extra,
pending: None,
})
} else {
None
}
}
/// Commit any changes to storage.
pub fn commit(&mut self) -> Result<(), ()> {
if let Some(extra) = self.pending.take() {
Account::<T>::try_mutate_exists(self.id, self.who.borrow(), |maybe_account|
if let Some(ref mut account) = maybe_account {
account.extra = extra;
Ok(())
} else {
Err(())
}
)
} else {
Ok(())
}
}
/// Revert any changes, even those already committed by `self` and drop self.
pub fn revert(mut self) -> Result<(), ()> {
self.pending = None;
Account::<T>::try_mutate_exists(self.id, self.who.borrow(), |maybe_account|
if let Some(ref mut account) = maybe_account {
account.extra = self.original.clone();
Ok(())
} else {
Err(())
}
)
}
}
+469
View File
@@ -0,0 +1,469 @@
// 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.
//! Functions for the Assets pallet.
use super::*;
// The main implementation block for the module.
impl<T: Config> Pallet<T> {
// Public immutables
/// Return the extra "sid-car" data for `id`/`who`, or `None` if the account doesn't exist.
pub fn adjust_extra(id: T::AssetId, who: impl sp_std::borrow::Borrow<T::AccountId>)
-> Option<ExtraMutator<T>>
{
ExtraMutator::maybe_new(id, who)
}
/// Get the asset `id` balance of `who`.
pub fn balance(id: T::AssetId, who: impl sp_std::borrow::Borrow<T::AccountId>) -> T::Balance {
Account::<T>::get(id, who.borrow()).balance
}
/// Get the total supply of an asset `id`.
pub fn total_supply(id: T::AssetId) -> T::Balance {
Asset::<T>::get(id).map(|x| x.supply).unwrap_or_else(Zero::zero)
}
pub(super) fn new_account(
who: &T::AccountId,
d: &mut AssetDetails<T::Balance, T::AccountId, DepositBalanceOf<T>>,
) -> Result<bool, DispatchError> {
let accounts = d.accounts.checked_add(1).ok_or(Error::<T>::Overflow)?;
let is_sufficient = if d.is_sufficient {
frame_system::Pallet::<T>::inc_sufficients(who);
d.sufficients += 1;
true
} else {
frame_system::Pallet::<T>::inc_consumers(who).map_err(|_| Error::<T>::NoProvider)?;
false
};
d.accounts = accounts;
Ok(is_sufficient)
}
pub(super) fn dead_account(
what: T::AssetId,
who: &T::AccountId,
d: &mut AssetDetails<T::Balance, T::AccountId, DepositBalanceOf<T>>,
sufficient: bool,
) {
if sufficient {
d.sufficients = d.sufficients.saturating_sub(1);
frame_system::Pallet::<T>::dec_sufficients(who);
} else {
frame_system::Pallet::<T>::dec_consumers(who);
}
d.accounts = d.accounts.saturating_sub(1);
T::Freezer::died(what, who)
}
pub(super) fn can_increase(id: T::AssetId, who: &T::AccountId, amount: T::Balance) -> DepositConsequence {
let details = match Asset::<T>::get(id) {
Some(details) => details,
None => return DepositConsequence::UnknownAsset,
};
if details.supply.checked_add(&amount).is_none() {
return DepositConsequence::Overflow
}
let account = Account::<T>::get(id, who);
if account.balance.checked_add(&amount).is_none() {
return DepositConsequence::Overflow
}
if account.balance.is_zero() {
if amount < details.min_balance {
return DepositConsequence::BelowMinimum
}
if !details.is_sufficient && frame_system::Pallet::<T>::providers(who) == 0 {
return DepositConsequence::CannotCreate
}
if details.is_sufficient && details.sufficients.checked_add(1).is_none() {
return DepositConsequence::Overflow
}
}
DepositConsequence::Success
}
/// Return the consequence of a withdraw.
pub(super) fn can_decrease(
id: T::AssetId,
who: &T::AccountId,
amount: T::Balance,
keep_alive: bool,
) -> WithdrawConsequence<T::Balance> {
use WithdrawConsequence::*;
let details = match Asset::<T>::get(id) {
Some(details) => details,
None => return UnknownAsset,
};
if details.supply.checked_sub(&amount).is_none() {
return Underflow
}
if details.is_frozen {
return Frozen
}
let account = Account::<T>::get(id, who);
if account.is_frozen {
return Frozen
}
if let Some(rest) = account.balance.checked_sub(&amount) {
if let Some(frozen) = T::Freezer::frozen_balance(id, who) {
match frozen.checked_add(&details.min_balance) {
Some(required) if rest < required => return Frozen,
None => return Overflow,
_ => {}
}
}
let is_provider = false;
let is_required = is_provider && !frame_system::Pallet::<T>::can_dec_provider(who);
let must_keep_alive = keep_alive || is_required;
if rest < details.min_balance {
if must_keep_alive {
WouldDie
} else {
ReducedToZero(rest)
}
} else {
Success
}
} else {
NoFunds
}
}
// Maximum `amount` that can be passed into `can_withdraw` to result in a `WithdrawConsequence`
// of `Success`.
pub(super) fn reducible_balance(
id: T::AssetId,
who: &T::AccountId,
keep_alive: bool,
) -> Result<T::Balance, Error<T>> {
let details = match Asset::<T>::get(id) {
Some(details) => details,
None => return Err(Error::<T>::Unknown),
};
ensure!(!details.is_frozen, Error::<T>::Frozen);
let account = Account::<T>::get(id, who);
ensure!(!account.is_frozen, Error::<T>::Frozen);
let amount = if let Some(frozen) = T::Freezer::frozen_balance(id, who) {
// Frozen balance: account CANNOT be deleted
let required = frozen.checked_add(&details.min_balance).ok_or(Error::<T>::Overflow)?;
account.balance.saturating_sub(required)
} else {
let is_provider = false;
let is_required = is_provider && !frame_system::Pallet::<T>::can_dec_provider(who);
if keep_alive || is_required {
// We want to keep the account around.
account.balance.saturating_sub(details.min_balance)
} else {
// Don't care if the account dies
account.balance
}
};
Ok(amount.min(details.supply))
}
/// Make preparatory checks for debiting some funds from an account. Flags indicate requirements
/// of the debit.
///
/// - `amount`: The amount desired to be debited. The actual amount returned for debit may be
/// less (in the case of `best_effort` being `true`) or greater by up to the minimum balance
/// less one.
/// - `keep_alive`: Require that `target` must stay alive.
/// - `respect_freezer`: Respect any freezes on the account or token (or not).
/// - `best_effort`: The debit amount may be less than `amount`.
///
/// On success, the amount which should be debited (this will always be at least `amount` unless
/// `best_effort` is `true`) together with an optional value indicating the argument which must
/// be passed into the `melted` function of the `T::Freezer` if `Some`.
///
/// If no valid debit can be made then return an `Err`.
pub(super) fn prep_debit(
id: T::AssetId,
target: &T::AccountId,
amount: T::Balance,
f: DebitFlags,
) -> Result<T::Balance, DispatchError> {
let actual = Self::reducible_balance(id, target, f.keep_alive)?
.min(amount);
ensure!(f.best_effort || actual >= amount, Error::<T>::BalanceLow);
let conseq = Self::can_decrease(id, target, actual, f.keep_alive);
let actual = match conseq.into_result() {
Ok(dust) => actual.saturating_add(dust), //< guaranteed by reducible_balance
Err(e) => {
debug_assert!(false, "passed from reducible_balance; qed");
return Err(e.into())
}
};
Ok(actual)
}
/// Make preparatory checks for crediting some funds from an account. Flags indicate
/// requirements of the credit.
///
/// - `amount`: The amount desired to be credited.
/// - `debit`: The amount by which some other account has been debited. If this is greater than
/// `amount`, then the `burn_dust` parameter takes effect.
/// - `burn_dust`: Indicates that in the case of debit being greater than amount, the additional
/// (dust) value should be burned, rather than credited.
///
/// On success, the amount which should be credited (this will always be at least `amount`)
/// together with an optional value indicating the value which should be burned. The latter
/// will always be `None` as long as `burn_dust` is `false` or `debit` is no greater than
/// `amount`.
///
/// If no valid credit can be made then return an `Err`.
pub(super) fn prep_credit(
id: T::AssetId,
dest: &T::AccountId,
amount: T::Balance,
debit: T::Balance,
burn_dust: bool,
) -> Result<(T::Balance, Option<T::Balance>), DispatchError> {
let (credit, maybe_burn) = match (burn_dust, debit.checked_sub(&amount)) {
(true, Some(dust)) => (amount, Some(dust)),
_ => (debit, None),
};
Self::can_increase(id, &dest, credit).into_result()?;
Ok((credit, maybe_burn))
}
/// Increases the asset `id` balance of `beneficiary` by `amount`.
///
/// This alters the registered supply of the asset and emits an event.
///
/// Will return an error or will increase the amount by exactly `amount`.
pub(super) fn do_mint(
id: T::AssetId,
beneficiary: &T::AccountId,
amount: T::Balance,
maybe_check_issuer: Option<T::AccountId>,
) -> DispatchResult {
Self::increase_balance(id, beneficiary, amount, |details| -> DispatchResult {
if let Some(check_issuer) = maybe_check_issuer {
ensure!(&check_issuer == &details.issuer, Error::<T>::NoPermission);
}
debug_assert!(T::Balance::max_value() - details.supply >= amount, "checked in prep; qed");
details.supply = details.supply.saturating_add(amount);
Ok(())
})?;
Self::deposit_event(Event::Issued(id, beneficiary.clone(), amount));
Ok(())
}
/// Increases the asset `id` balance of `beneficiary` by `amount`.
///
/// LOW-LEVEL: Does not alter the supply of asset or emit an event. Use `do_mint` if you need
/// that. This is not intended to be used alone.
///
/// Will return an error or will increase the amount by exactly `amount`.
pub(super) fn increase_balance(
id: T::AssetId,
beneficiary: &T::AccountId,
amount: T::Balance,
check: impl FnOnce(&mut AssetDetails<T::Balance, T::AccountId, DepositBalanceOf<T>>) -> DispatchResult,
) -> DispatchResult {
if amount.is_zero() { return Ok(()) }
Self::can_increase(id, beneficiary, amount).into_result()?;
Asset::<T>::try_mutate(id, |maybe_details| -> DispatchResult {
let details = maybe_details.as_mut().ok_or(Error::<T>::Unknown)?;
check(details)?;
Account::<T>::try_mutate(id, beneficiary, |t| -> DispatchResult {
let new_balance = t.balance.saturating_add(amount);
ensure!(new_balance >= details.min_balance, TokenError::BelowMinimum);
if t.balance.is_zero() {
t.sufficient = Self::new_account(beneficiary, details)?;
}
t.balance = new_balance;
Ok(())
})?;
Ok(())
})?;
Ok(())
}
/// Reduces asset `id` balance of `target` by `amount`. Flags `f` can be given to alter whether
/// it attempts a `best_effort` or makes sure to `keep_alive` the account.
///
/// This alters the registered supply of the asset and emits an event.
///
/// Will return an error and do nothing or will decrease the amount and return the amount
/// reduced by.
pub(super) fn do_burn(
id: T::AssetId,
target: &T::AccountId,
amount: T::Balance,
maybe_check_admin: Option<T::AccountId>,
f: DebitFlags,
) -> Result<T::Balance, DispatchError> {
let actual = Self::decrease_balance(id, target, amount, f, |actual, details| {
// Check admin rights.
if let Some(check_admin) = maybe_check_admin {
ensure!(&check_admin == &details.admin, Error::<T>::NoPermission);
}
debug_assert!(details.supply >= actual, "checked in prep; qed");
details.supply = details.supply.saturating_sub(actual);
Ok(())
})?;
Self::deposit_event(Event::Burned(id, target.clone(), actual));
Ok(actual)
}
/// Reduces asset `id` balance of `target` by `amount`. Flags `f` can be given to alter whether
/// it attempts a `best_effort` or makes sure to `keep_alive` the account.
///
/// LOW-LEVEL: Does not alter the supply of asset or emit an event. Use `do_burn` if you need
/// that. This is not intended to be used alone.
///
/// Will return an error and do nothing or will decrease the amount and return the amount
/// reduced by.
pub(super) fn decrease_balance(
id: T::AssetId,
target: &T::AccountId,
amount: T::Balance,
f: DebitFlags,
check: impl FnOnce(
T::Balance,
&mut AssetDetails<T::Balance, T::AccountId, DepositBalanceOf<T>>,
) -> DispatchResult,
) -> Result<T::Balance, DispatchError> {
if amount.is_zero() { return Ok(amount) }
let actual = Self::prep_debit(id, target, amount, f)?;
Asset::<T>::try_mutate(id, |maybe_details| -> DispatchResult {
let details = maybe_details.as_mut().ok_or(Error::<T>::Unknown)?;
check(actual, details)?;
Account::<T>::try_mutate_exists(id, target, |maybe_account| -> DispatchResult {
let mut account = maybe_account.take().unwrap_or_default();
debug_assert!(account.balance >= actual, "checked in prep; qed");
// Make the debit.
account.balance = account.balance.saturating_sub(actual);
*maybe_account = if account.balance < details.min_balance {
debug_assert!(account.balance.is_zero(), "checked in prep; qed");
Self::dead_account(id, target, details, account.sufficient);
None
} else {
Some(account)
};
Ok(())
})?;
Ok(())
})?;
Ok(actual)
}
/// Reduces the asset `id` balance of `source` by some `amount` and increases the balance of
/// `dest` by (similar) amount.
///
/// Returns the actual amount placed into `dest`. Exact semantics are determined by the flags
/// `f`.
///
/// Will fail if the amount transferred is so small that it cannot create the destination due
/// to minimum balance requirements.
pub(super) fn do_transfer(
id: T::AssetId,
source: &T::AccountId,
dest: &T::AccountId,
amount: T::Balance,
maybe_need_admin: Option<T::AccountId>,
f: TransferFlags,
) -> Result<T::Balance, DispatchError> {
// Early exist if no-op.
if amount.is_zero() {
Self::deposit_event(Event::Transferred(id, source.clone(), dest.clone(), amount));
return Ok(amount)
}
// Figure out the debit and credit, together with side-effects.
let debit = Self::prep_debit(id, &source, amount, f.into())?;
let (credit, maybe_burn) = Self::prep_credit(id, &dest, amount, debit, f.burn_dust)?;
let mut source_account = Account::<T>::get(id, &source);
Asset::<T>::try_mutate(id, |maybe_details| -> DispatchResult {
let details = maybe_details.as_mut().ok_or(Error::<T>::Unknown)?;
// Check admin rights.
if let Some(need_admin) = maybe_need_admin {
ensure!(&need_admin == &details.admin, Error::<T>::NoPermission);
}
// Skip if source == dest
if source == dest {
return Ok(())
}
// Burn any dust if needed.
if let Some(burn) = maybe_burn {
// Debit dust from supply; this will not saturate since it's already checked in prep.
debug_assert!(details.supply >= burn, "checked in prep; qed");
details.supply = details.supply.saturating_sub(burn);
}
// Debit balance from source; this will not saturate since it's already checked in prep.
debug_assert!(source_account.balance >= debit, "checked in prep; qed");
source_account.balance = source_account.balance.saturating_sub(debit);
Account::<T>::try_mutate(id, &dest, |a| -> DispatchResult {
// Calculate new balance; this will not saturate since it's already checked in prep.
debug_assert!(a.balance.checked_add(&credit).is_some(), "checked in prep; qed");
let new_balance = a.balance.saturating_add(credit);
// Create a new account if there wasn't one already.
if a.balance.is_zero() {
a.sufficient = Self::new_account(&dest, details)?;
}
a.balance = new_balance;
Ok(())
})?;
// Remove source account if it's now dead.
if source_account.balance < details.min_balance {
debug_assert!(source_account.balance.is_zero(), "checked in prep; qed");
Self::dead_account(id, &source, details, source_account.sufficient);
Account::<T>::remove(id, &source);
} else {
Account::<T>::insert(id, &source, &source_account)
}
Ok(())
})?;
Self::deposit_event(Event::Transferred(id, source.clone(), dest.clone(), credit));
Ok(credit)
}
}
@@ -0,0 +1,153 @@
// 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.
//! Implementations for fungibles trait.
use super::*;
impl<T: Config> fungibles::Inspect<<T as SystemConfig>::AccountId> for Pallet<T> {
type AssetId = T::AssetId;
type Balance = T::Balance;
fn total_issuance(asset: Self::AssetId) -> Self::Balance {
Asset::<T>::get(asset).map(|x| x.supply).unwrap_or_else(Zero::zero)
}
fn minimum_balance(asset: Self::AssetId) -> Self::Balance {
Asset::<T>::get(asset).map(|x| x.min_balance).unwrap_or_else(Zero::zero)
}
fn balance(
asset: Self::AssetId,
who: &<T as SystemConfig>::AccountId,
) -> Self::Balance {
Pallet::<T>::balance(asset, who)
}
fn reducible_balance(
asset: Self::AssetId,
who: &<T as SystemConfig>::AccountId,
keep_alive: bool,
) -> Self::Balance {
Pallet::<T>::reducible_balance(asset, who, keep_alive).unwrap_or(Zero::zero())
}
fn can_deposit(
asset: Self::AssetId,
who: &<T as SystemConfig>::AccountId,
amount: Self::Balance,
) -> DepositConsequence {
Pallet::<T>::can_increase(asset, who, amount)
}
fn can_withdraw(
asset: Self::AssetId,
who: &<T as SystemConfig>::AccountId,
amount: Self::Balance,
) -> WithdrawConsequence<Self::Balance> {
Pallet::<T>::can_decrease(asset, who, amount, false)
}
}
impl<T: Config> fungibles::Mutate<<T as SystemConfig>::AccountId> for Pallet<T> {
fn mint_into(
asset: Self::AssetId,
who: &<T as SystemConfig>::AccountId,
amount: Self::Balance,
) -> DispatchResult {
Self::do_mint(asset, who, amount, None)
}
fn burn_from(
asset: Self::AssetId,
who: &<T as SystemConfig>::AccountId,
amount: Self::Balance,
) -> Result<Self::Balance, DispatchError> {
let f = DebitFlags {
keep_alive: false,
best_effort: false,
};
Self::do_burn(asset, who, amount, None, f)
}
fn slash(
asset: Self::AssetId,
who: &<T as SystemConfig>::AccountId,
amount: Self::Balance,
) -> Result<Self::Balance, DispatchError> {
let f = DebitFlags {
keep_alive: false,
best_effort: true,
};
Self::do_burn(asset, who, amount, None, f)
}
}
impl<T: Config> fungibles::Transfer<T::AccountId> for Pallet<T> {
fn transfer(
asset: Self::AssetId,
source: &T::AccountId,
dest: &T::AccountId,
amount: T::Balance,
keep_alive: bool,
) -> Result<T::Balance, DispatchError> {
let f = TransferFlags {
keep_alive,
best_effort: false,
burn_dust: false
};
Self::do_transfer(asset, source, dest, amount, None, f)
}
}
impl<T: Config> fungibles::Unbalanced<T::AccountId> for Pallet<T> {
fn set_balance(_: Self::AssetId, _: &T::AccountId, _: Self::Balance) -> DispatchResult {
unreachable!("set_balance is not used if other functions are impl'd");
}
fn set_total_issuance(id: T::AssetId, amount: Self::Balance) {
Asset::<T>::mutate_exists(id, |maybe_asset| if let Some(ref mut asset) = maybe_asset {
asset.supply = amount
});
}
fn decrease_balance(asset: T::AssetId, who: &T::AccountId, amount: Self::Balance)
-> Result<Self::Balance, DispatchError>
{
let f = DebitFlags { keep_alive: false, best_effort: false };
Self::decrease_balance(asset, who, amount, f, |_, _| Ok(()))
}
fn decrease_balance_at_most(asset: T::AssetId, who: &T::AccountId, amount: Self::Balance)
-> Self::Balance
{
let f = DebitFlags { keep_alive: false, best_effort: true };
Self::decrease_balance(asset, who, amount, f, |_, _| Ok(()))
.unwrap_or(Zero::zero())
}
fn increase_balance(asset: T::AssetId, who: &T::AccountId, amount: Self::Balance)
-> Result<Self::Balance, DispatchError>
{
Self::increase_balance(asset, who, amount, |_| Ok(()))?;
Ok(amount)
}
fn increase_balance_at_most(asset: T::AssetId, who: &T::AccountId, amount: Self::Balance)
-> Self::Balance
{
match Self::increase_balance(asset, who, amount, |_| Ok(())) {
Ok(()) => amount,
Err(_) => Zero::zero(),
}
}
}
@@ -0,0 +1,58 @@
// 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's `StoredMap` implementation.
use super::*;
impl<T: Config> StoredMap<(T::AssetId, T::AccountId), T::Extra> for Pallet<T> {
fn get(id_who: &(T::AssetId, T::AccountId)) -> T::Extra {
let &(id, ref who) = id_who;
if Account::<T>::contains_key(id, who) {
Account::<T>::get(id, who).extra
} else {
Default::default()
}
}
fn try_mutate_exists<R, E: From<StoredMapError>>(
id_who: &(T::AssetId, T::AccountId),
f: impl FnOnce(&mut Option<T::Extra>) -> Result<R, E>,
) -> Result<R, E> {
let &(id, ref who) = id_who;
let mut maybe_extra = Some(Account::<T>::get(id, who).extra);
let r = f(&mut maybe_extra)?;
// They want to write some value or delete it.
// If the account existed and they want to write a value, then we write.
// If the account didn't exist and they want to delete it, then we let it pass.
// Otherwise, we fail.
Account::<T>::try_mutate_exists(id, who, |maybe_account| {
if let Some(extra) = maybe_extra {
// They want to write a value. Let this happen only if the account actually exists.
if let Some(ref mut account) = maybe_account {
account.extra = extra;
} else {
Err(StoredMapError::NoProviders)?;
}
} else {
// They want to delete it. Let this pass if the item never existed anyway.
ensure!(maybe_account.is_none(), StoredMapError::ConsumerRemaining);
}
Ok(r)
})
}
}
+54 -402
View File
@@ -130,191 +130,30 @@ pub mod mock;
#[cfg(test)]
mod tests;
use sp_std::prelude::*;
mod extra_mutator;
pub use extra_mutator::*;
mod impl_stored_map;
mod impl_fungibles;
mod functions;
mod types;
pub use types::*;
use sp_std::{prelude::*, borrow::Borrow};
use sp_runtime::{
RuntimeDebug,
traits::{
AtLeast32BitUnsigned, Zero, StaticLookup, Saturating, CheckedSub, CheckedAdd,
RuntimeDebug, TokenError, traits::{
AtLeast32BitUnsigned, Zero, StaticLookup, Saturating, CheckedSub, CheckedAdd, Bounded,
StoredMapError,
}
};
use codec::{Encode, Decode, HasCompact};
use frame_support::{ensure, dispatch::{DispatchError, DispatchResult}};
use frame_support::traits::{Currency, ReservableCurrency, BalanceStatus::Reserved};
use frame_support::traits::{Currency, ReservableCurrency, BalanceStatus::Reserved, StoredMap};
use frame_support::traits::tokens::{WithdrawConsequence, DepositConsequence, fungibles};
use frame_system::Config as SystemConfig;
pub use weights::WeightInfo;
pub use pallet::*;
impl<T: Config> fungibles::Inspect<<T as SystemConfig>::AccountId> for Pallet<T> {
type AssetId = T::AssetId;
type Balance = T::Balance;
fn total_issuance(asset: Self::AssetId) -> Self::Balance {
Asset::<T>::get(asset).map(|x| x.supply).unwrap_or_else(Zero::zero)
}
fn minimum_balance(asset: Self::AssetId) -> Self::Balance {
Asset::<T>::get(asset).map(|x| x.min_balance).unwrap_or_else(Zero::zero)
}
fn balance(
asset: Self::AssetId,
who: &<T as SystemConfig>::AccountId,
) -> Self::Balance {
Pallet::<T>::balance(asset, who)
}
fn can_deposit(
asset: Self::AssetId,
who: &<T as SystemConfig>::AccountId,
amount: Self::Balance,
) -> DepositConsequence {
Pallet::<T>::can_deposit(asset, who, amount)
}
fn can_withdraw(
asset: Self::AssetId,
who: &<T as SystemConfig>::AccountId,
amount: Self::Balance,
) -> WithdrawConsequence<Self::Balance> {
Pallet::<T>::can_withdraw(asset, who, amount)
}
}
impl<T: Config> fungibles::Mutate<<T as SystemConfig>::AccountId> for Pallet<T> {
fn deposit(
asset: Self::AssetId,
who: &<T as SystemConfig>::AccountId,
amount: Self::Balance,
) -> DispatchResult {
Pallet::<T>::increase_balance(asset, who.clone(), amount, None)
}
fn withdraw(
asset: Self::AssetId,
who: &<T as SystemConfig>::AccountId,
amount: Self::Balance,
) -> Result<Self::Balance, DispatchError> {
Pallet::<T>::reduce_balance(asset, who.clone(), amount, None)
}
}
impl<T: Config> fungibles::Transfer<T::AccountId> for Pallet<T> {
fn transfer(
asset: Self::AssetId,
source: &T::AccountId,
dest: &T::AccountId,
amount: T::Balance,
) -> Result<T::Balance, DispatchError> {
<Self as fungibles::Mutate::<T::AccountId>>::transfer(asset, source, dest, amount)
}
}
type DepositBalanceOf<T> = <<T as Config>::Currency as Currency<<T as SystemConfig>::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<Balance, AccountId, DepositBalance> AssetDetails<Balance, AccountId, DepositBalance> {
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<AccountId> {
/// 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<Balance, DepositBalance> {
/// 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<Balance> {
/// 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<DepositBalance> {
/// 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<u8>,
/// The ticker symbol for this asset. Limited in length by `StringLimit`.
symbol: Vec<u8>,
/// 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::{
@@ -363,6 +202,13 @@ pub mod pallet {
/// The maximum length of a name or symbol stored on-chain.
type StringLimit: Get<u32>;
/// 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<Self::AssetId, Self::AccountId, Self::Balance>;
/// Additional data to be stored with an account's asset balance.
type Extra: Member + Parameter + Default;
/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
}
@@ -384,7 +230,7 @@ pub mod pallet {
T::AssetId,
Blake2_128Concat,
T::AccountId,
AssetBalance<T::Balance>,
AssetBalance<T::Balance, T::Extra>,
ValueQuery,
>;
@@ -637,7 +483,7 @@ pub mod pallet {
ensure!(details.approvals == witness.approvals, Error::<T>::BadWitness);
for (who, v) in Account::<T>::drain_prefix(id) {
Self::dead_account(&who, &mut details, v.sufficient);
Self::dead_account(id, &who, &mut details, v.sufficient);
}
debug_assert_eq!(details.accounts, 0);
debug_assert_eq!(details.sufficients, 0);
@@ -674,7 +520,9 @@ pub mod pallet {
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let beneficiary = T::Lookup::lookup(beneficiary)?;
Self::increase_balance(id, beneficiary, amount, Some(origin))
Self::do_mint(id, &beneficiary, amount, Some(origin))?;
Self::deposit_event(Event::Issued(id, beneficiary, amount));
Ok(())
}
/// Reduce the balance of `who` by as much as possible up to `amount` assets of `id`.
@@ -702,7 +550,10 @@ pub mod pallet {
let origin = ensure_signed(origin)?;
let who = T::Lookup::lookup(who)?;
Self::reduce_balance(id, who, amount, Some(origin)).map(|_| ())
let f = DebitFlags { keep_alive: false, best_effort: true };
let burned = Self::do_burn(id, &who, amount, Some(origin), f)?;
Self::deposit_event(Event::Burned(id, who, burned));
Ok(())
}
/// Move some assets from the sender account to another.
@@ -733,7 +584,12 @@ pub mod pallet {
let origin = ensure_signed(origin)?;
let dest = T::Lookup::lookup(target)?;
Self::do_transfer(id, origin, dest, amount, None, false)
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.
@@ -761,10 +617,15 @@ pub mod pallet {
target: <T::Lookup as StaticLookup>::Source,
#[pallet::compact] amount: T::Balance
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let source = ensure_signed(origin)?;
let dest = T::Lookup::lookup(target)?;
Self::do_transfer(id, origin, dest, amount, None, true)
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.
@@ -798,7 +659,12 @@ pub mod pallet {
let source = T::Lookup::lookup(source)?;
let dest = T::Lookup::lookup(dest)?;
Self::do_transfer(id, source, dest, amount, Some(origin), false)
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.
@@ -1351,7 +1217,12 @@ pub mod pallet {
let mut approved = maybe_approved.take().ok_or(Error::<T>::Unapproved)?;
let remaining = approved.amount.checked_sub(&amount).ok_or(Error::<T>::Unapproved)?;
Self::do_transfer(id, key.owner.clone(), destination, amount, None, false)?;
let f = TransferFlags {
keep_alive: false,
best_effort: false,
burn_dust: false
};
Self::do_transfer(id, &key.owner, &destination, amount, None, f)?;
if remaining.is_zero() {
T::Currency::unreserve(&key.owner, approved.deposit);
@@ -1365,222 +1236,3 @@ pub mod pallet {
}
}
}
// The main implementation block for the module.
impl<T: Config> Pallet<T> {
// Public immutables
/// Get the asset `id` balance of `who`.
pub fn balance(id: T::AssetId, who: impl sp_std::borrow::Borrow<T::AccountId>) -> T::Balance {
Account::<T>::get(id, who.borrow()).balance
}
/// Get the total supply of an asset `id`.
pub fn total_supply(id: T::AssetId) -> T::Balance {
Asset::<T>::get(id).map(|x| x.supply).unwrap_or_else(Zero::zero)
}
fn new_account(
who: &T::AccountId,
d: &mut AssetDetails<T::Balance, T::AccountId, DepositBalanceOf<T>>,
) -> Result<bool, DispatchError> {
let accounts = d.accounts.checked_add(1).ok_or(Error::<T>::Overflow)?;
let is_sufficient = if d.is_sufficient {
frame_system::Pallet::<T>::inc_sufficients(who);
d.sufficients += 1;
true
} else {
frame_system::Pallet::<T>::inc_consumers(who).map_err(|_| Error::<T>::NoProvider)?;
false
};
d.accounts = accounts;
Ok(is_sufficient)
}
fn dead_account(
who: &T::AccountId,
d: &mut AssetDetails<T::Balance, T::AccountId, DepositBalanceOf<T>>,
sufficient: bool,
) {
if sufficient {
d.sufficients = d.sufficients.saturating_sub(1);
frame_system::Pallet::<T>::dec_sufficients(who);
} else {
frame_system::Pallet::<T>::dec_consumers(who);
}
d.accounts = d.accounts.saturating_sub(1);
}
fn can_deposit(id: T::AssetId, who: &T::AccountId, amount: T::Balance) -> DepositConsequence {
let details = match Asset::<T>::get(id) {
Some(details) => details,
None => return DepositConsequence::UnknownAsset,
};
if details.supply.checked_add(&amount).is_none() {
return DepositConsequence::Overflow
}
let account = Account::<T>::get(id, who);
if account.balance.checked_add(&amount).is_none() {
return DepositConsequence::Overflow
}
if account.balance.is_zero() {
if amount < details.min_balance {
return DepositConsequence::BelowMinimum
}
if !details.is_sufficient && frame_system::Pallet::<T>::providers(who) == 0 {
return DepositConsequence::CannotCreate
}
if details.is_sufficient && details.sufficients.checked_add(1).is_none() {
return DepositConsequence::Overflow
}
}
DepositConsequence::Success
}
fn can_withdraw(
id: T::AssetId,
who: &T::AccountId,
amount: T::Balance,
) -> WithdrawConsequence<T::Balance> {
let details = match Asset::<T>::get(id) {
Some(details) => details,
None => return WithdrawConsequence::UnknownAsset,
};
if details.supply.checked_sub(&amount).is_none() {
return WithdrawConsequence::Underflow
}
let account = Account::<T>::get(id, who);
if let Some(rest) = account.balance.checked_sub(&amount) {
if rest < details.min_balance {
WithdrawConsequence::ReducedToZero(rest)
} else {
// NOTE: this assumes (correctly) that the token won't be a provider. If that ever
// changes, this will need to change.
WithdrawConsequence::Success
}
} else {
WithdrawConsequence::NoFunds
}
}
fn increase_balance(
id: T::AssetId,
beneficiary: T::AccountId,
amount: T::Balance,
maybe_check_issuer: Option<T::AccountId>,
) -> DispatchResult {
Asset::<T>::try_mutate(id, |maybe_details| {
let details = maybe_details.as_mut().ok_or(Error::<T>::Unknown)?;
if let Some(check_issuer) = maybe_check_issuer {
ensure!(&check_issuer == &details.issuer, Error::<T>::NoPermission);
}
details.supply = details.supply.checked_add(&amount).ok_or(Error::<T>::Overflow)?;
Account::<T>::try_mutate(id, &beneficiary, |t| -> DispatchResult {
let new_balance = t.balance.saturating_add(amount);
ensure!(new_balance >= details.min_balance, Error::<T>::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<T::AccountId>,
) -> Result<T::Balance, DispatchError> {
Asset::<T>::try_mutate(id, |maybe_details| {
let d = maybe_details.as_mut().ok_or(Error::<T>::Unknown)?;
if let Some(check_admin) = maybe_check_admin {
ensure!(&check_admin == &d.admin, Error::<T>::NoPermission);
}
let burned = Account::<T>::try_mutate_exists(
id,
&target,
|maybe_account| -> Result<T::Balance, DispatchError> {
let mut account = maybe_account.take().ok_or(Error::<T>::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(burned)
})
}
fn do_transfer(
id: T::AssetId,
source: T::AccountId,
dest: T::AccountId,
amount: T::Balance,
maybe_need_admin: Option<T::AccountId>,
keep_alive: bool,
) -> DispatchResult {
let mut source_account = Account::<T>::get(id, &source);
ensure!(!source_account.is_frozen, Error::<T>::Frozen);
source_account.balance = source_account.balance.checked_sub(&amount)
.ok_or(Error::<T>::BalanceLow)?;
Asset::<T>::try_mutate(id, |maybe_details| {
let details = maybe_details.as_mut().ok_or(Error::<T>::Unknown)?;
ensure!(!details.is_frozen, Error::<T>::Frozen);
if let Some(need_admin) = maybe_need_admin {
ensure!(&need_admin == &details.admin, Error::<T>::NoPermission);
}
if dest != source && !amount.is_zero() {
let mut amount = amount;
if source_account.balance < details.min_balance {
ensure!(!keep_alive, Error::<T>::WouldDie);
amount += source_account.balance;
source_account.balance = Zero::zero();
}
Account::<T>::try_mutate(id, &dest, |a| -> DispatchResult {
let new_balance = a.balance.saturating_add(amount);
ensure!(new_balance >= details.min_balance, Error::<T>::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::<T>::remove(id, &source);
} else {
Account::<T>::insert(id, &source, &source_account)
}
}
Self::deposit_event(Event::Transferred(id, source, dest, amount));
Ok(())
})
}
}
+35
View File
@@ -100,7 +100,42 @@ impl Config for Test {
type MetadataDepositPerByte = MetadataDepositPerByte;
type ApprovalDeposit = ApprovalDeposit;
type StringLimit = StringLimit;
type Freezer = TestFreezer;
type WeightInfo = ();
type Extra = ();
}
use std::cell::RefCell;
use std::collections::HashMap;
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub(crate) enum Hook {
Died(u32, u64),
}
thread_local! {
static FROZEN: RefCell<HashMap<(u32, u64), u64>> = RefCell::new(Default::default());
static HOOKS: RefCell<Vec<Hook>> = RefCell::new(Default::default());
}
pub struct TestFreezer;
impl FrozenBalance<u32, u64, u64> for TestFreezer {
fn frozen_balance(asset: u32, who: &u64) -> Option<u64> {
FROZEN.with(|f| f.borrow().get(&(asset, who.clone())).cloned())
}
fn died(asset: u32, who: &u64) {
HOOKS.with(|h| h.borrow_mut().push(Hook::Died(asset, who.clone())));
}
}
pub(crate) fn set_frozen_balance(asset: u32, who: u64, amount: u64) {
FROZEN.with(|f| f.borrow_mut().insert((asset, who), amount));
}
pub(crate) fn clear_frozen_balance(asset: u32, who: u64) {
FROZEN.with(|f| f.borrow_mut().remove(&(asset, who)));
}
pub(crate) fn hooks() -> Vec<Hook> {
HOOKS.with(|h| h.borrow().clone())
}
pub(crate) fn new_test_ext() -> sp_io::TestExternalities {
+75 -10
View File
@@ -19,6 +19,7 @@
use super::*;
use crate::{Error, mock::*};
use sp_runtime::TokenError;
use frame_support::{assert_ok, assert_noop, traits::Currency};
use pallet_balances::Error as BalancesError;
@@ -198,11 +199,11 @@ fn non_providing_should_work() {
assert_ok!(Assets::mint(Origin::signed(1), 0, 0, 100));
// Cannot mint into account 2 since it doesn't (yet) exist...
assert_noop!(Assets::mint(Origin::signed(1), 0, 1, 100), Error::<Test>::NoProvider);
assert_noop!(Assets::mint(Origin::signed(1), 0, 1, 100), TokenError::CannotCreate);
// ...or transfer...
assert_noop!(Assets::transfer(Origin::signed(0), 0, 1, 50), Error::<Test>::NoProvider);
assert_noop!(Assets::transfer(Origin::signed(0), 0, 1, 50), TokenError::CannotCreate);
// ...or force-transfer
assert_noop!(Assets::force_transfer(Origin::signed(1), 0, 0, 1, 50), Error::<Test>::NoProvider);
assert_noop!(Assets::force_transfer(Origin::signed(1), 0, 0, 1, 50), TokenError::CannotCreate);
Balances::make_free_balance_be(&1, 100);
Balances::make_free_balance_be(&2, 100);
@@ -219,12 +220,11 @@ fn min_balance_should_work() {
assert_eq!(Asset::<Test>::get(0).unwrap().accounts, 1);
// Cannot create a new account with a balance that is below minimum...
assert_noop!(Assets::mint(Origin::signed(1), 0, 2, 9), Error::<Test>::BalanceLow);
assert_noop!(Assets::transfer(Origin::signed(1), 0, 2, 9), Error::<Test>::BalanceLow);
assert_noop!(Assets::force_transfer(Origin::signed(1), 0, 1, 2, 9), Error::<Test>::BalanceLow);
assert_noop!(Assets::mint(Origin::signed(1), 0, 2, 9), TokenError::BelowMinimum);
assert_noop!(Assets::transfer(Origin::signed(1), 0, 2, 9), TokenError::BelowMinimum);
assert_noop!(Assets::force_transfer(Origin::signed(1), 0, 1, 2, 9), TokenError::BelowMinimum);
// When deducting from an account to below minimum, it should be reaped.
assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 91));
assert!(Assets::balance(0, 1).is_zero());
assert_eq!(Assets::balance(0, 2), 100);
@@ -277,7 +277,7 @@ fn transferring_enough_to_kill_source_when_keep_alive_should_fail() {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, true, 10));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_eq!(Assets::balance(0, 1), 100);
assert_noop!(Assets::transfer_keep_alive(Origin::signed(1), 0, 2, 91), Error::<Test>::WouldDie);
assert_noop!(Assets::transfer_keep_alive(Origin::signed(1), 0, 2, 91), Error::<Test>::BalanceLow);
assert_ok!(Assets::transfer_keep_alive(Origin::signed(1), 0, 2, 90));
assert_eq!(Assets::balance(0, 1), 10);
assert_eq!(Assets::balance(0, 2), 90);
@@ -430,12 +430,14 @@ fn burning_asset_balance_with_positive_balance_should_work() {
}
#[test]
fn burning_asset_balance_with_zero_balance_should_not_work() {
fn burning_asset_balance_with_zero_balance_does_nothing() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, true, 1));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_eq!(Assets::balance(0, 2), 0);
assert_noop!(Assets::burn(Origin::signed(1), 0, 2, u64::max_value()), Error::<Test>::BalanceZero);
assert_ok!(Assets::burn(Origin::signed(1), 0, 2, u64::max_value()));
assert_eq!(Assets::balance(0, 2), 0);
assert_eq!(Assets::total_supply(0), 100);
});
}
@@ -491,3 +493,66 @@ fn set_metadata_should_work() {
}
// TODO: tests for force_set_metadata, force_clear_metadata, force_asset_status
// https://github.com/paritytech/substrate/issues/8470
#[test]
fn freezer_should_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, true, 10));
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
assert_eq!(Assets::balance(0, 1), 100);
// freeze 50 of it.
set_frozen_balance(0, 1, 50);
assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 20));
// cannot transfer another 21 away as this would take the non-frozen balance (30) to below
// the minimum balance (10).
assert_noop!(Assets::transfer(Origin::signed(1), 0, 2, 21), Error::<Test>::BalanceLow);
// create an approved transfer...
Balances::make_free_balance_be(&1, 100);
assert_ok!(Assets::approve_transfer(Origin::signed(1), 0, 2, 50));
let e = Error::<Test>::BalanceLow;
// ...but that wont work either:
assert_noop!(Assets::transfer_approved(Origin::signed(2), 0, 1, 2, 21), e);
// a force transfer won't work also.
let e = Error::<Test>::BalanceLow;
assert_noop!(Assets::force_transfer(Origin::signed(1), 0, 1, 2, 21), e);
// reduce it to only 49 frozen...
set_frozen_balance(0, 1, 49);
// ...and it's all good:
assert_ok!(Assets::force_transfer(Origin::signed(1), 0, 1, 2, 21));
// and if we clear it, we can remove the account completely.
clear_frozen_balance(0, 1);
assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 50));
assert_eq!(hooks(), vec![Hook::Died(0, 1)]);
});
}
#[test]
fn imbalances_should_work() {
use frame_support::traits::tokens::fungibles::Balanced;
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(Origin::root(), 0, 1, true, 1));
let imb = Assets::issue(0, 100);
assert_eq!(Assets::total_supply(0), 100);
assert_eq!(imb.peek(), 100);
let (imb1, imb2) = imb.split(30);
assert_eq!(imb1.peek(), 30);
assert_eq!(imb2.peek(), 70);
drop(imb2);
assert_eq!(Assets::total_supply(0), 30);
assert!(Assets::resolve(&1, imb1).is_ok());
assert_eq!(Assets::balance(0, 1), 30);
assert_eq!(Assets::total_supply(0), 30);
});
}
+186
View File
@@ -0,0 +1,186 @@
// 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.
//! Various basic tyoes for use in the assets pallet.
use super::*;
pub(super) type DepositBalanceOf<T> = <<T as Config>::Currency as Currency<<T as SystemConfig>::AccountId>>::Balance;
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug)]
pub struct AssetDetails<
Balance,
AccountId,
DepositBalance,
> {
/// Can change `owner`, `issuer`, `freezer` and `admin` accounts.
pub(super) owner: AccountId,
/// Can mint tokens.
pub(super) issuer: AccountId,
/// Can thaw tokens, force transfers and burn tokens from any account.
pub(super) admin: AccountId,
/// Can freeze tokens.
pub(super) freezer: AccountId,
/// The total supply across all accounts.
pub(super) supply: Balance,
/// The balance deposited for this asset. This pays for the data stored here.
pub(super) deposit: DepositBalance,
/// The ED for virtual accounts.
pub(super) min_balance: Balance,
/// If `true`, then any account with this asset is given a provider reference. Otherwise, it
/// requires a consumer reference.
pub(super) is_sufficient: bool,
/// The total number of accounts.
pub(super) accounts: u32,
/// The total number of accounts for which we have placed a self-sufficient reference.
pub(super) sufficients: u32,
/// The total number of approvals.
pub(super) approvals: u32,
/// Whether the asset is frozen for non-admin transfers.
pub(super) is_frozen: bool,
}
impl<Balance, AccountId, DepositBalance> AssetDetails<Balance, AccountId, DepositBalance> {
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<AccountId> {
/// The owner of the funds that are being approved.
pub(super) owner: AccountId,
/// The party to whom transfer of the funds is being delegated.
pub(super) delegate: AccountId,
}
/// Data concerning an approval.
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default)]
pub struct Approval<Balance, DepositBalance> {
/// The amount of funds approved for the balance transfer from the owner to some delegated
/// target.
pub(super) amount: Balance,
/// The amount reserved on the owner's account to hold this item in storage.
pub(super) deposit: DepositBalance,
}
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default)]
pub struct AssetBalance<Balance, Extra> {
/// The balance.
pub(super) balance: Balance,
/// Whether the account is frozen.
pub(super) is_frozen: bool,
/// `true` if this balance gave the account a self-sufficient reference.
pub(super) sufficient: bool,
/// Additional "sidecar" data, in case some other pallet wants to use this storage item.
pub(super) extra: Extra,
}
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default)]
pub struct AssetMetadata<DepositBalance> {
/// The balance deposited for this metadata.
///
/// This pays for the data stored in this struct.
pub(super) deposit: DepositBalance,
/// The user friendly name of this asset. Limited in length by `StringLimit`.
pub(super) name: Vec<u8>,
/// The ticker symbol for this asset. Limited in length by `StringLimit`.
pub(super) symbol: Vec<u8>,
/// The number of decimals this asset uses to represent one unit.
pub(super) decimals: u8,
/// Whether the asset metadata may be changed by a non Force origin.
pub(super) 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)]
pub(super) accounts: u32,
/// The number of accounts holding the asset with a self-sufficient reference.
#[codec(compact)]
pub(super) sufficients: u32,
/// The number of transfer-approvals of the asset.
#[codec(compact)]
pub(super) approvals: u32,
}
/// Trait for allowing a minimum balance on the account to be specified, beyond the
/// `minimum_balance` of the asset. This is additive - the `minimum_balance` of the asset must be
/// met *and then* anything here in addition.
pub trait FrozenBalance<AssetId, AccountId, Balance> {
/// Return the frozen balance. Under normal behaviour, this amount should always be
/// withdrawable.
///
/// In reality, the balance of every account must be at least the sum of this (if `Some`) and
/// the asset's minimum_balance, since there may be complications to destroying an asset's
/// account completely.
///
/// If `None` is returned, then nothing special is enforced.
///
/// If any operation ever breaks this requirement (which will only happen through some sort of
/// privileged intervention), then `melted` is called to do any cleanup.
fn frozen_balance(asset: AssetId, who: &AccountId) -> Option<Balance>;
/// Called when an account has been removed.
fn died(asset: AssetId, who: &AccountId);
}
impl<AssetId, AccountId, Balance> FrozenBalance<AssetId, AccountId, Balance> for () {
fn frozen_balance(_: AssetId, _: &AccountId) -> Option<Balance> { None }
fn died(_: AssetId, _: &AccountId) {}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub(super) struct TransferFlags {
/// The debited account must stay alive at the end of the operation; an error is returned if
/// this cannot be achieved legally.
pub(super) keep_alive: bool,
/// Less than the amount specified needs be debited by the operation for it to be considered
/// successful. If `false`, then the amount debited will always be at least the amount
/// specified.
pub(super) best_effort: bool,
/// Any additional funds debited (due to minimum balance requirements) should be burned rather
/// than credited to the destination account.
pub(super) burn_dust: bool,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub(super) struct DebitFlags {
/// The debited account must stay alive at the end of the operation; an error is returned if
/// this cannot be achieved legally.
pub(super) keep_alive: bool,
/// Less than the amount specified needs be debited by the operation for it to be considered
/// successful. If `false`, then the amount debited will always be at least the amount
/// specified.
pub(super) best_effort: bool,
}
impl From<TransferFlags> for DebitFlags {
fn from(f: TransferFlags) -> Self {
Self {
keep_alive: f.keep_alive,
best_effort: f.best_effort,
}
}
}
+131 -45
View File
@@ -39,7 +39,7 @@
//! ### Terminology
//!
//! - **Existential Deposit:** The minimum balance required to create or keep an account open. This prevents
//! "dust accounts" from filling storage. When the free plus the reserved balance (i.e. the total balance)
//! "dust accounts" from filling storage. When the free plus the reserved balance (i.e. the total balance)
//! fall below this, then the account is said to be dead; and it loses its functionality as well as any
//! prior history and all information on it is removed from the chain's state.
//! No account should ever have a total balance that is strictly between 0 and the existential
@@ -164,7 +164,8 @@ use frame_support::{
Currency, OnUnbalanced, TryDrop, StoredMap,
WithdrawReasons, LockIdentifier, LockableCurrency, ExistenceRequirement,
Imbalance, SignedImbalance, ReservableCurrency, Get, ExistenceRequirement::KeepAlive,
ExistenceRequirement::AllowDeath, BalanceStatus as Status,
ExistenceRequirement::AllowDeath,
tokens::{fungible, DepositConsequence, WithdrawConsequence, BalanceStatus as Status}
}
};
#[cfg(feature = "std")]
@@ -764,7 +765,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// the caller will do this.
pub fn mutate_account<R>(
who: &T::AccountId,
f: impl FnOnce(&mut AccountData<T::Balance>) -> R
f: impl FnOnce(&mut AccountData<T::Balance>) -> R,
) -> Result<R, StoredMapError> {
Self::try_mutate_account(who, |a, _| -> Result<R, StoredMapError> { Ok(f(a)) })
}
@@ -780,7 +781,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// the caller will do this.
fn try_mutate_account<R, E: From<StoredMapError>>(
who: &T::AccountId,
f: impl FnOnce(&mut AccountData<T::Balance>, bool) -> Result<R, E>
f: impl FnOnce(&mut AccountData<T::Balance>, bool) -> Result<R, E>,
) -> Result<R, E> {
Self::try_mutate_account_with_dust(who, f)
.map(|(result, dust_cleaner)| {
@@ -804,7 +805,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// the caller will do this.
fn try_mutate_account_with_dust<R, E: From<StoredMapError>>(
who: &T::AccountId,
f: impl FnOnce(&mut AccountData<T::Balance>, bool) -> Result<R, E>
f: impl FnOnce(&mut AccountData<T::Balance>, bool) -> Result<R, E>,
) -> Result<(R, DustCleaner<T, I>), E> {
let result = T::AccountStore::try_mutate_exists(who, |maybe_account| {
let is_new = maybe_account.is_none();
@@ -873,9 +874,57 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
}
}
}
}
use frame_support::traits::tokens::{fungible, DepositConsequence, WithdrawConsequence};
/// Move the reserved balance of one account into the balance of another, according to `status`.
///
/// Is a no-op if:
/// - the value to be moved is zero; or
/// - the `slashed` id equal to `beneficiary` and the `status` is `Reserved`.
fn do_transfer_reserved(
slashed: &T::AccountId,
beneficiary: &T::AccountId,
value: T::Balance,
best_effort: bool,
status: Status,
) -> Result<T::Balance, DispatchError> {
if value.is_zero() { return Ok(Zero::zero()) }
if slashed == beneficiary {
return match status {
Status::Free => Ok(Self::unreserve(slashed, value)),
Status::Reserved => Ok(value.saturating_sub(Self::reserved_balance(slashed))),
};
}
let ((actual, _maybe_one_dust), _maybe_other_dust) = Self::try_mutate_account_with_dust(
beneficiary,
|to_account, is_new| -> Result<(T::Balance, DustCleaner<T, I>), DispatchError> {
ensure!(!is_new, Error::<T, I>::DeadAccount);
Self::try_mutate_account_with_dust(
slashed,
|from_account, _| -> Result<T::Balance, DispatchError> {
let actual = cmp::min(from_account.reserved, value);
ensure!(best_effort || actual == value, Error::<T, I>::InsufficientBalance);
match status {
Status::Free => to_account.free = to_account.free
.checked_add(&actual)
.ok_or(Error::<T, I>::Overflow)?,
Status::Reserved => to_account.reserved = to_account.reserved
.checked_add(&actual)
.ok_or(Error::<T, I>::Overflow)?,
}
from_account.reserved -= actual;
Ok(actual)
}
)
}
)?;
Self::deposit_event(Event::ReserveRepatriated(slashed.clone(), beneficiary.clone(), actual, status));
Ok(actual)
}
}
impl<T: Config<I>, I: 'static> fungible::Inspect<T::AccountId> for Pallet<T, I> {
type Balance = T::Balance;
@@ -889,6 +938,19 @@ impl<T: Config<I>, I: 'static> fungible::Inspect<T::AccountId> for Pallet<T, I>
fn balance(who: &T::AccountId) -> Self::Balance {
Self::account(who).total()
}
fn reducible_balance(who: &T::AccountId, keep_alive: bool) -> Self::Balance {
let a = Self::account(who);
// Liquid balance is what is neither reserved nor locked/frozen.
let liquid = a.free.saturating_sub(a.fee_frozen.max(a.misc_frozen));
if frame_system::Pallet::<T>::can_dec_provider(who) && !keep_alive {
liquid
} else {
// `must_remain_to_exist` is the part of liquid balance which must remain to keep total over
// ED.
let must_remain_to_exist = T::ExistentialDeposit::get().saturating_sub(a.total() - liquid);
liquid.saturating_sub(must_remain_to_exist)
}
}
fn can_deposit(who: &T::AccountId, amount: Self::Balance) -> DepositConsequence {
Self::deposit_consequence(who, amount, &Self::account(who))
}
@@ -898,7 +960,7 @@ impl<T: Config<I>, I: 'static> fungible::Inspect<T::AccountId> for Pallet<T, I>
}
impl<T: Config<I>, I: 'static> fungible::Mutate<T::AccountId> for Pallet<T, I> {
fn deposit(who: &T::AccountId, amount: Self::Balance) -> DispatchResult {
fn mint_into(who: &T::AccountId, amount: Self::Balance) -> DispatchResult {
if amount.is_zero() { return Ok(()) }
Self::try_mutate_account(who, |account, _is_new| -> DispatchResult {
Self::deposit_consequence(who, amount, &account).into_result()?;
@@ -909,9 +971,8 @@ impl<T: Config<I>, I: 'static> fungible::Mutate<T::AccountId> for Pallet<T, I> {
Ok(())
}
fn withdraw(who: &T::AccountId, amount: Self::Balance) -> Result<Self::Balance, DispatchError> {
fn burn_from(who: &T::AccountId, amount: Self::Balance) -> Result<Self::Balance, DispatchError> {
if amount.is_zero() { return Ok(Self::Balance::zero()); }
let actual = Self::try_mutate_account(who, |account, _is_new| -> Result<T::Balance, DispatchError> {
let extra = Self::withdraw_consequence(who, amount, &account).into_result()?;
let actual = amount + extra;
@@ -928,8 +989,11 @@ impl<T: Config<I>, I: 'static> fungible::Transfer<T::AccountId> for Pallet<T, I>
source: &T::AccountId,
dest: &T::AccountId,
amount: T::Balance,
keep_alive: bool,
) -> Result<T::Balance, DispatchError> {
<Self as fungible::Mutate::<T::AccountId>>::transfer(source, dest, amount)
let er = if keep_alive { KeepAlive } else { AllowDeath };
<Self as Currency::<T::AccountId>>::transfer(source, dest, amount, er)
.map(|_| amount)
}
}
@@ -944,6 +1008,60 @@ impl<T: Config<I>, I: 'static> fungible::Unbalanced<T::AccountId> for Pallet<T,
}
}
impl<T: Config<I>, I: 'static> fungible::InspectHold<T::AccountId> for Pallet<T, I> {
fn balance_on_hold(who: &T::AccountId) -> T::Balance {
Self::account(who).reserved
}
fn can_hold(who: &T::AccountId, amount: T::Balance) -> bool {
let a = Self::account(who);
let min_balance = T::ExistentialDeposit::get().max(a.frozen(Reasons::All));
if a.reserved.checked_add(&amount).is_none() { return false }
// We require it to be min_balance + amount to ensure that the full reserved funds may be
// slashed without compromising locked funds or destroying the account.
let required_free = match min_balance.checked_add(&amount) {
Some(x) => x,
None => return false,
};
a.free >= required_free
}
}
impl<T: Config<I>, I: 'static> fungible::MutateHold<T::AccountId> for Pallet<T, I> {
fn hold(who: &T::AccountId, amount: Self::Balance) -> DispatchResult {
if amount.is_zero() { return Ok(()) }
ensure!(Self::can_reserve(who, amount), Error::<T, I>::InsufficientBalance);
Self::mutate_account(who, |a| {
a.free -= amount;
a.reserved += amount;
})?;
Ok(())
}
fn release(who: &T::AccountId, amount: Self::Balance, best_effort: bool)
-> Result<T::Balance, DispatchError>
{
if amount.is_zero() { return Ok(amount) }
// Done on a best-effort basis.
Self::try_mutate_account(who, |a, _| {
let new_free = a.free.saturating_add(amount.min(a.reserved));
let actual = new_free - a.free;
ensure!(best_effort || actual == amount, Error::<T, I>::InsufficientBalance);
// ^^^ Guaranteed to be <= amount and <= a.reserved
a.free = new_free;
a.reserved = a.reserved.saturating_sub(actual.clone());
Ok(actual)
})
}
fn transfer_held(
source: &T::AccountId,
dest: &T::AccountId,
amount: Self::Balance,
best_effort: bool,
on_hold: bool,
) -> Result<Self::Balance, DispatchError> {
let status = if on_hold { Status::Reserved } else { Status::Free };
Self::do_transfer_reserved(source, dest, amount, best_effort, status)
}
}
// wrapping these imbalances in a private module is necessary to ensure absolute privacy
// of the inner member.
mod imbalances {
@@ -1521,40 +1639,8 @@ impl<T: Config<I>, I: 'static> ReservableCurrency<T::AccountId> for Pallet<T, I>
value: Self::Balance,
status: Status,
) -> Result<Self::Balance, DispatchError> {
if value.is_zero() { return Ok(Zero::zero()) }
if slashed == beneficiary {
return match status {
Status::Free => Ok(Self::unreserve(slashed, value)),
Status::Reserved => Ok(value.saturating_sub(Self::reserved_balance(slashed))),
};
}
let ((actual, _maybe_one_dust), _maybe_other_dust) = Self::try_mutate_account_with_dust(
beneficiary,
|to_account, is_new| -> Result<(Self::Balance, DustCleaner<T, I>), DispatchError> {
ensure!(!is_new, Error::<T, I>::DeadAccount);
Self::try_mutate_account_with_dust(
slashed,
|from_account, _| -> Result<Self::Balance, DispatchError> {
let actual = cmp::min(from_account.reserved, value);
match status {
Status::Free => to_account.free = to_account.free
.checked_add(&actual)
.ok_or(Error::<T, I>::Overflow)?,
Status::Reserved => to_account.reserved = to_account.reserved
.checked_add(&actual)
.ok_or(Error::<T, I>::Overflow)?,
}
from_account.reserved -= actual;
Ok(actual)
}
)
}
)?;
Self::deposit_event(Event::ReserveRepatriated(slashed.clone(), beneficiary.clone(), actual, status));
Ok(value - actual)
let actual = Self::do_transfer_reserved(slashed, beneficiary, value, true, status)?;
Ok(value.saturating_sub(actual))
}
}
+2 -9
View File
@@ -20,15 +20,8 @@
//! NOTE: If you're looking for `parameter_types`, it has moved in to the top-level module.
pub mod tokens;
pub use tokens::fungible::{
Inspect as InspectFungible, Mutate as MutateFungible, Transfer as TransferFungible,
Reserve as ReserveFungible, Balanced as BalancedFungible, Unbalanced as UnbalancedFungible,
ItemOf,
};
pub use tokens::fungibles::{
Inspect as InspectFungibles, Mutate as MutateFungibles, Transfer as TransferFungibles,
Reserve as ReserveFungibles, Balanced as BalancedFungibles, Unbalanced as UnbalancedFungibles,
};
pub use tokens::fungible;
pub use tokens::fungibles;
pub use tokens::currency::{
Currency, LockIdentifier, LockableCurrency, ReservableCurrency, VestingSchedule,
};
@@ -32,14 +32,22 @@ pub use imbalance::{Imbalance, HandleImbalanceDrop, DebtOf, CreditOf};
pub trait Inspect<AccountId> {
/// Scalar type for representing balance of an account.
type Balance: Balance;
/// The total amount of issuance in the system.
fn total_issuance() -> Self::Balance;
/// The minimum balance any single account may have.
fn minimum_balance() -> Self::Balance;
/// Get the balance of `who`.
fn balance(who: &AccountId) -> Self::Balance;
/// Get the maximum amount that `who` can withdraw/transfer successfully.
fn reducible_balance(who: &AccountId, keep_alive: bool) -> Self::Balance;
/// Returns `true` if the balance of `who` may be increased by `amount`.
fn can_deposit(who: &AccountId, amount: Self::Balance) -> DepositConsequence;
/// Returns `Failed` if the balance of `who` may not be decreased by `amount`, otherwise
/// the consequence.
fn can_withdraw(who: &AccountId, amount: Self::Balance) -> WithdrawConsequence<Self::Balance>;
@@ -47,26 +55,42 @@ pub trait Inspect<AccountId> {
/// Trait for providing an ERC-20 style fungible asset.
pub trait Mutate<AccountId>: Inspect<AccountId> {
/// Increase the balance of `who` by `amount`.
fn deposit(who: &AccountId, amount: Self::Balance) -> DispatchResult;
/// Attempt to reduce the balance of `who` by `amount`.
fn withdraw(who: &AccountId, amount: Self::Balance) -> Result<Self::Balance, DispatchError>;
/// Transfer funds from one account into another.
fn transfer(
/// Increase the balance of `who` by exactly `amount`, minting new tokens. If that isn't
/// possible then an `Err` is returned and nothing is changed.
fn mint_into(who: &AccountId, amount: Self::Balance) -> DispatchResult;
/// Decrease the balance of `who` by at least `amount`, possibly slightly more in the case of
/// minimum_balance requirements, burning the tokens. If that isn't possible then an `Err` is
/// returned and nothing is changed. If successful, the amount of tokens reduced is returned.
fn burn_from(who: &AccountId, amount: Self::Balance) -> Result<Self::Balance, DispatchError>;
/// Attempt to reduce the balance of `who` by as much as possible up to `amount`, and possibly
/// slightly more due to minimum_balance requirements. If no decrease is possible then an `Err`
/// is returned and nothing is changed. If successful, the amount of tokens reduced is returned.
///
/// The default implementation just uses `withdraw` along with `reducible_balance` to ensure
/// that is doesn't fail.
fn slash(who: &AccountId, amount: Self::Balance) -> Result<Self::Balance, DispatchError> {
Self::burn_from(who, Self::reducible_balance(who, false).min(amount))
}
/// Transfer funds from one account into another. The default implementation uses `mint_into`
/// and `burn_from` and may generate unwanted events.
fn teleport(
source: &AccountId,
dest: &AccountId,
amount: Self::Balance,
) -> Result<Self::Balance, DispatchError> {
let extra = Self::can_withdraw(&source, amount).into_result()?;
Self::can_deposit(&dest, amount.saturating_add(extra)).into_result()?;
let actual = Self::withdraw(source, amount)?;
let actual = Self::burn_from(source, amount)?;
debug_assert!(actual == amount.saturating_add(extra), "can_withdraw must agree with withdraw; qed");
match Self::deposit(dest, actual) {
match Self::mint_into(dest, actual) {
Ok(_) => Ok(actual),
Err(err) => {
debug_assert!(false, "can_deposit returned true previously; qed");
// attempt to return the funds back to source
let revert = Self::deposit(source, actual);
let revert = Self::mint_into(source, actual);
debug_assert!(revert.is_ok(), "withdrew funds previously; qed");
Err(err)
}
@@ -81,31 +105,82 @@ pub trait Transfer<AccountId>: Inspect<AccountId> {
source: &AccountId,
dest: &AccountId,
amount: Self::Balance,
keep_alive: bool,
) -> Result<Self::Balance, DispatchError>;
}
/// Trait for providing a fungible asset which can be reserved.
pub trait Reserve<AccountId>: Inspect<AccountId> {
/// Trait for inspecting a fungible asset which can be reserved.
pub trait InspectHold<AccountId>: Inspect<AccountId> {
/// Amount of funds held in reserve by `who`.
fn reserved_balance(who: &AccountId) -> Self::Balance;
/// Amount of funds held in total by `who`.
fn total_balance(who: &AccountId) -> Self::Balance {
Self::reserved_balance(who).saturating_add(Self::balance(who))
}
/// Check to see if some `amount` of funds may be reserved on the account of `who`.
fn can_reserve(who: &AccountId, amount: Self::Balance) -> bool;
/// Reserve some funds in an account.
fn reserve(who: &AccountId, amount: Self::Balance) -> DispatchResult;
/// Unreserve some funds in an account.
fn unreserve(who: &AccountId, amount: Self::Balance) -> DispatchResult;
/// Transfer reserved funds into another account.
fn repatriate_reserved(
who: &AccountId,
amount: Self::Balance,
status: BalanceStatus,
) -> DispatchResult;
fn balance_on_hold(who: &AccountId) -> Self::Balance;
/// Check to see if some `amount` of funds of `who` may be placed on hold.
fn can_hold(who: &AccountId, amount: Self::Balance) -> bool;
}
/// Trait for mutating a fungible asset which can be reserved.
pub trait MutateHold<AccountId>: InspectHold<AccountId> + Transfer<AccountId> {
/// Hold some funds in an account.
fn hold(who: &AccountId, amount: Self::Balance) -> DispatchResult;
/// Release up to `amount` held funds in an account.
///
/// The actual amount released is returned with `Ok`.
///
/// If `best_effort` is `true`, then the amount actually unreserved and returned as the inner
/// value of `Ok` may be smaller than the `amount` passed.
fn release(who: &AccountId, amount: Self::Balance, best_effort: bool)
-> Result<Self::Balance, DispatchError>;
/// Transfer held funds into a destination account.
///
/// If `on_hold` is `true`, then the destination account must already exist and the assets
/// transferred will still be on hold in the destination account. If not, then the destination
/// account need not already exist, but must be creatable.
///
/// If `best_effort` is `true`, then an amount less than `amount` may be transferred without
/// error.
///
/// The actual amount transferred is returned, or `Err` in the case of error and nothing is
/// changed.
fn transfer_held(
source: &AccountId,
dest: &AccountId,
amount: Self::Balance,
best_effort: bool,
on_held: bool,
) -> Result<Self::Balance, DispatchError>;
}
/// Trait for slashing a fungible asset which can be reserved.
pub trait BalancedHold<AccountId>: Balanced<AccountId> + MutateHold<AccountId> {
/// Reduce the balance of some funds on hold in an account.
///
/// The resulting imbalance is the first item of the tuple returned.
///
/// As much funds that are on hold up to `amount` will be deducted as possible. If this is less
/// than `amount`, then a non-zero second item will be returned.
fn slash_held(who: &AccountId, amount: Self::Balance)
-> (CreditOf<AccountId, Self>, Self::Balance);
}
impl<
AccountId,
T: Balanced<AccountId> + MutateHold<AccountId>,
> BalancedHold<AccountId> for T {
fn slash_held(who: &AccountId, amount: Self::Balance)
-> (CreditOf<AccountId, Self>, Self::Balance)
{
let actual = match Self::release(who, amount, true) {
Ok(x) => x,
Err(_) => return (Imbalance::default(), amount),
};
<Self as fungible::Balanced<AccountId>>::slash(who, actual)
}
}
/// Convert a `fungibles` trait implementation into a `fungible` trait implementation by identifying
/// a single item.
pub struct ItemOf<
F: fungibles::Inspect<AccountId>,
A: Get<<F as fungibles::Inspect<AccountId>>::AssetId>,
@@ -129,6 +204,9 @@ impl<
fn balance(who: &AccountId) -> Self::Balance {
<F as fungibles::Inspect<AccountId>>::balance(A::get(), who)
}
fn reducible_balance(who: &AccountId, keep_alive: bool) -> Self::Balance {
<F as fungibles::Inspect<AccountId>>::reducible_balance(A::get(), who, keep_alive)
}
fn can_deposit(who: &AccountId, amount: Self::Balance) -> DepositConsequence {
<F as fungibles::Inspect<AccountId>>::can_deposit(A::get(), who, amount)
}
@@ -142,11 +220,11 @@ impl<
A: Get<<F as fungibles::Inspect<AccountId>>::AssetId>,
AccountId,
> Mutate<AccountId> for ItemOf<F, A, AccountId> {
fn deposit(who: &AccountId, amount: Self::Balance) -> DispatchResult {
<F as fungibles::Mutate<AccountId>>::deposit(A::get(), who, amount)
fn mint_into(who: &AccountId, amount: Self::Balance) -> DispatchResult {
<F as fungibles::Mutate<AccountId>>::mint_into(A::get(), who, amount)
}
fn withdraw(who: &AccountId, amount: Self::Balance) -> Result<Self::Balance, DispatchError> {
<F as fungibles::Mutate<AccountId>>::withdraw(A::get(), who, amount)
fn burn_from(who: &AccountId, amount: Self::Balance) -> Result<Self::Balance, DispatchError> {
<F as fungibles::Mutate<AccountId>>::burn_from(A::get(), who, amount)
}
}
@@ -155,39 +233,54 @@ impl<
A: Get<<F as fungibles::Inspect<AccountId>>::AssetId>,
AccountId,
> Transfer<AccountId> for ItemOf<F, A, AccountId> {
fn transfer(source: &AccountId, dest: &AccountId, amount: Self::Balance)
fn transfer(source: &AccountId, dest: &AccountId, amount: Self::Balance, keep_alive: bool)
-> Result<Self::Balance, DispatchError>
{
<F as fungibles::Transfer<AccountId>>::transfer(A::get(), source, dest, amount)
<F as fungibles::Transfer<AccountId>>::transfer(A::get(), source, dest, amount, keep_alive)
}
}
impl<
F: fungibles::Reserve<AccountId>,
F: fungibles::InspectHold<AccountId>,
A: Get<<F as fungibles::Inspect<AccountId>>::AssetId>,
AccountId,
> Reserve<AccountId> for ItemOf<F, A, AccountId> {
fn reserved_balance(who: &AccountId) -> Self::Balance {
<F as fungibles::Reserve<AccountId>>::reserved_balance(A::get(), who)
> InspectHold<AccountId> for ItemOf<F, A, AccountId> {
fn balance_on_hold(who: &AccountId) -> Self::Balance {
<F as fungibles::InspectHold<AccountId>>::balance_on_hold(A::get(), who)
}
fn total_balance(who: &AccountId) -> Self::Balance {
<F as fungibles::Reserve<AccountId>>::total_balance(A::get(), who)
fn can_hold(who: &AccountId, amount: Self::Balance) -> bool {
<F as fungibles::InspectHold<AccountId>>::can_hold(A::get(), who, amount)
}
fn can_reserve(who: &AccountId, amount: Self::Balance) -> bool {
<F as fungibles::Reserve<AccountId>>::can_reserve(A::get(), who, amount)
}
impl<
F: fungibles::MutateHold<AccountId>,
A: Get<<F as fungibles::Inspect<AccountId>>::AssetId>,
AccountId,
> MutateHold<AccountId> for ItemOf<F, A, AccountId> {
fn hold(who: &AccountId, amount: Self::Balance) -> DispatchResult {
<F as fungibles::MutateHold<AccountId>>::hold(A::get(), who, amount)
}
fn reserve(who: &AccountId, amount: Self::Balance) -> DispatchResult {
<F as fungibles::Reserve<AccountId>>::reserve(A::get(), who, amount)
fn release(who: &AccountId, amount: Self::Balance, best_effort: bool)
-> Result<Self::Balance, DispatchError>
{
<F as fungibles::MutateHold<AccountId>>::release(A::get(), who, amount, best_effort)
}
fn unreserve(who: &AccountId, amount: Self::Balance) -> DispatchResult {
<F as fungibles::Reserve<AccountId>>::unreserve(A::get(), who, amount)
}
fn repatriate_reserved(
who: &AccountId,
fn transfer_held(
source: &AccountId,
dest: &AccountId,
amount: Self::Balance,
status: BalanceStatus,
) -> DispatchResult {
<F as fungibles::Reserve<AccountId>>::repatriate_reserved(A::get(), who, amount, status)
best_effort: bool,
on_hold: bool,
) -> Result<Self::Balance, DispatchError> {
<F as fungibles::MutateHold<AccountId>>::transfer_held(
A::get(),
source,
dest,
amount,
best_effort,
on_hold,
)
}
}
@@ -215,4 +308,3 @@ impl<
<F as fungibles::Unbalanced<AccountId>>::increase_balance_at_most(A::get(), who, amount)
}
}
@@ -55,14 +55,11 @@ pub trait Balanced<AccountId>: Inspect<AccountId> {
///
/// This is just the same as burning and issuing the same amount and has no effect on the
/// total issuance.
fn pair(amount: Self::Balance)
-> (DebtOf<AccountId, Self>, CreditOf<AccountId, Self>)
{
fn pair(amount: Self::Balance) -> (DebtOf<AccountId, Self>, CreditOf<AccountId, Self>) {
(Self::rescind(amount), Self::issue(amount))
}
/// Deducts up to `value` from the combined balance of `who`, preferring to deduct from the
/// free balance. This function cannot fail.
/// Deducts up to `value` from the combined balance of `who`. This function cannot fail.
///
/// The resulting imbalance is the first item of the tuple returned.
///
@@ -31,17 +31,26 @@ pub use imbalance::{Imbalance, HandleImbalanceDrop, DebtOf, CreditOf};
pub trait Inspect<AccountId> {
/// Means of identifying one asset class from another.
type AssetId: AssetId;
/// Scalar type for representing balance of an account.
type Balance: Balance;
/// The total amount of issuance in the system.
fn total_issuance(asset: Self::AssetId) -> Self::Balance;
/// The minimum balance any single account may have.
fn minimum_balance(asset: Self::AssetId) -> Self::Balance;
/// Get the `asset` balance of `who`.
fn balance(asset: Self::AssetId, who: &AccountId) -> Self::Balance;
/// Get the maximum amount of `asset` that `who` can withdraw/transfer successfully.
fn reducible_balance(asset: Self::AssetId, who: &AccountId, keep_alive: bool) -> Self::Balance;
/// Returns `true` if the `asset` balance of `who` may be increased by `amount`.
fn can_deposit(asset: Self::AssetId, who: &AccountId, amount: Self::Balance)
-> DepositConsequence;
/// Returns `Failed` if the `asset` balance of `who` may not be decreased by `amount`, otherwise
/// the consequence.
fn can_withdraw(
@@ -62,7 +71,7 @@ pub trait Mutate<AccountId>: Inspect<AccountId> {
///
/// Since this is an operation which should be possible to take alone, if successful it will
/// increase the overall supply of the underlying token.
fn deposit(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) -> DispatchResult;
fn mint_into(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) -> DispatchResult;
/// Attempt to reduce the `asset` balance of `who` by `amount`.
///
@@ -78,11 +87,25 @@ pub trait Mutate<AccountId>: Inspect<AccountId> {
/// Due to minimum balance requirements, it's possible that the amount withdrawn could be up to
/// `Self::minimum_balance() - 1` more than the `amount`. The total amount withdrawn is returned
/// in an `Ok` result. This may be safely ignored if you don't mind the overall supply reducing.
fn withdraw(asset: Self::AssetId, who: &AccountId, amount: Self::Balance)
fn burn_from(asset: Self::AssetId, who: &AccountId, amount: Self::Balance)
-> Result<Self::Balance, DispatchError>;
/// Transfer funds from one account into another.
fn transfer(
/// Attempt to reduce the `asset` balance of `who` by as much as possible up to `amount`, and
/// possibly slightly more due to minimum_balance requirements. If no decrease is possible then
/// an `Err` is returned and nothing is changed. If successful, the amount of tokens reduced is
/// returned.
///
/// The default implementation just uses `withdraw` along with `reducible_balance` to ensure
/// that is doesn't fail.
fn slash(asset: Self::AssetId, who: &AccountId, amount: Self::Balance)
-> Result<Self::Balance, DispatchError>
{
Self::burn_from(asset, who, Self::reducible_balance(asset, who, false).min(amount))
}
/// Transfer funds from one account into another. The default implementation uses `mint_into`
/// and `burn_from` and may generate unwanted events.
fn teleport(
asset: Self::AssetId,
source: &AccountId,
dest: &AccountId,
@@ -90,14 +113,14 @@ pub trait Mutate<AccountId>: Inspect<AccountId> {
) -> Result<Self::Balance, DispatchError> {
let extra = Self::can_withdraw(asset, &source, amount).into_result()?;
Self::can_deposit(asset, &dest, amount.saturating_add(extra)).into_result()?;
let actual = Self::withdraw(asset, source, amount)?;
let actual = Self::burn_from(asset, source, amount)?;
debug_assert!(actual == amount.saturating_add(extra), "can_withdraw must agree with withdraw; qed");
match Self::deposit(asset, dest, actual) {
match Self::mint_into(asset, dest, actual) {
Ok(_) => Ok(actual),
Err(err) => {
debug_assert!(false, "can_deposit returned true previously; qed");
// attempt to return the funds back to source
let revert = Self::deposit(asset, source, actual);
let revert = Self::mint_into(asset, source, actual);
debug_assert!(revert.is_ok(), "withdrew funds previously; qed");
Err(err)
}
@@ -113,31 +136,75 @@ pub trait Transfer<AccountId>: Inspect<AccountId> {
source: &AccountId,
dest: &AccountId,
amount: Self::Balance,
keep_alive: bool,
) -> Result<Self::Balance, DispatchError>;
}
/// Trait for providing a set of named fungible assets which can be reserved.
pub trait Reserve<AccountId>: Inspect<AccountId> {
/// Amount of funds held in reserve.
fn reserved_balance(asset: Self::AssetId, who: &AccountId) -> Self::Balance;
/// Trait for inspecting a set of named fungible assets which can be placed on hold.
pub trait InspectHold<AccountId>: Inspect<AccountId> {
/// Amount of funds held in hold.
fn balance_on_hold(asset: Self::AssetId, who: &AccountId) -> Self::Balance;
/// Amount of funds held in reserve.
fn total_balance(asset: Self::AssetId, who: &AccountId) -> Self::Balance;
/// Check to see if some `amount` of `asset` may be reserved on the account of `who`.
fn can_reserve(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) -> bool;
/// Reserve some funds in an account.
fn reserve(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) -> DispatchResult;
/// Unreserve some funds in an account.
fn unreserve(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) -> DispatchResult;
/// Transfer reserved funds into another account.
fn repatriate_reserved(
asset: Self::AssetId,
who: &AccountId,
amount: Self::Balance,
status: BalanceStatus,
) -> DispatchResult;
/// Check to see if some `amount` of `asset` may be held on the account of `who`.
fn can_hold(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) -> bool;
}
/// Trait for mutating a set of named fungible assets which can be placed on hold.
pub trait MutateHold<AccountId>: InspectHold<AccountId> + Transfer<AccountId> {
/// Hold some funds in an account.
fn hold(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) -> DispatchResult;
/// Release some funds in an account from being on hold.
///
/// If `best_effort` is `true`, then the amount actually released and returned as the inner
/// value of `Ok` may be smaller than the `amount` passed.
fn release(asset: Self::AssetId, who: &AccountId, amount: Self::Balance, best_effort: bool)
-> Result<Self::Balance, DispatchError>;
/// Transfer held funds into a destination account.
///
/// If `on_hold` is `true`, then the destination account must already exist and the assets
/// transferred will still be on hold in the destination account. If not, then the destination
/// account need not already exist, but must be creatable.
///
/// If `best_effort` is `true`, then an amount less than `amount` may be transferred without
/// error.
///
/// The actual amount transferred is returned, or `Err` in the case of error and nothing is
/// changed.
fn transfer_held(
asset: Self::AssetId,
source: &AccountId,
dest: &AccountId,
amount: Self::Balance,
best_effort: bool,
on_hold: bool,
) -> Result<Self::Balance, DispatchError>;
}
/// Trait for mutating one of several types of fungible assets which can be held.
pub trait BalancedHold<AccountId>: Balanced<AccountId> + MutateHold<AccountId> {
/// Release and slash some funds in an account.
///
/// The resulting imbalance is the first item of the tuple returned.
///
/// As much funds up to `amount` will be deducted as possible. If this is less than `amount`,
/// then a non-zero second item will be returned.
fn slash_held(asset: Self::AssetId, who: &AccountId, amount: Self::Balance)
-> (CreditOf<AccountId, Self>, Self::Balance);
}
impl<
AccountId,
T: Balanced<AccountId> + MutateHold<AccountId>,
> BalancedHold<AccountId> for T {
fn slash_held(asset: Self::AssetId, who: &AccountId, amount: Self::Balance)
-> (CreditOf<AccountId, Self>, Self::Balance)
{
let actual = match Self::release(asset, who, amount, true) {
Ok(x) => x,
Err(_) => return (Imbalance::zero(asset), amount),
};
<Self as fungibles::Balanced<AccountId>>::slash(asset, who, actual)
}
}
@@ -30,7 +30,10 @@ use crate::traits::misc::{SameOrOther, TryDrop};
///
/// This is auto-implemented when a token class has `Unbalanced` implemented.
pub trait Balanced<AccountId>: Inspect<AccountId> {
/// The type for managing what happens when an instance of `Debt` is dropped without being used.
type OnDropDebt: HandleImbalanceDrop<Self::AssetId, Self::Balance>;
/// The type for managing what happens when an instance of `Credit` is dropped without being
/// used.
type OnDropCredit: HandleImbalanceDrop<Self::AssetId, Self::Balance>;
/// Reduce the total issuance by `amount` and return the according imbalance. The imbalance will
@@ -37,6 +37,9 @@ pub enum WithdrawConsequence<Balance> {
/// There has been an underflow in the system. This is indicative of a corrupt state and
/// likely unrecoverable.
Underflow,
/// There has been an overflow in the system. This is indicative of a corrupt state and
/// likely unrecoverable.
Overflow,
/// Not enough of the funds in the account are unavailable for withdrawal.
Frozen,
/// Account balance would reduce to zero, potentially destroying it. The parameter is the
@@ -56,6 +59,7 @@ impl<Balance: Zero> WithdrawConsequence<Balance> {
WouldDie => Err(TokenError::WouldDie),
UnknownAsset => Err(TokenError::UnknownAsset),
Underflow => Err(TokenError::Underflow),
Overflow => Err(TokenError::Overflow),
Frozen => Err(TokenError::Frozen),
ReducedToZero(result) => Ok(result),
Success => Ok(Zero::zero()),