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
+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,
}
}
}