diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index 5f09da33b7..83a2d4527b 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -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" diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index fcd120d000..f790cf41a4 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -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; } diff --git a/substrate/frame/assets/src/extra_mutator.rs b/substrate/frame/assets/src/extra_mutator.rs new file mode 100644 index 0000000000..26a9a3f357 --- /dev/null +++ b/substrate/frame/assets/src/extra_mutator.rs @@ -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 { + id: T::AssetId, + who: T::AccountId, + original: T::Extra, + pending: Option, +} + +impl Drop for ExtraMutator { + fn drop(&mut self) { + debug_assert!(self.commit().is_ok(), "attempt to write to non-existent asset account"); + } +} + +impl sp_std::ops::Deref for ExtraMutator { + type Target = T::Extra; + fn deref(&self) -> &T::Extra { + match self.pending { + Some(ref value) => value, + None => &self.original, + } + } +} + +impl sp_std::ops::DerefMut for ExtraMutator { + 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 ExtraMutator { + pub(super) fn maybe_new(id: T::AssetId, who: impl sp_std::borrow::Borrow) + -> Option> + { + if Account::::contains_key(id, who.borrow()) { + Some(ExtraMutator:: { + id, + who: who.borrow().clone(), + original: Account::::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::::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::::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(()) + } + ) + } +} diff --git a/substrate/frame/assets/src/functions.rs b/substrate/frame/assets/src/functions.rs new file mode 100644 index 0000000000..197b010b6e --- /dev/null +++ b/substrate/frame/assets/src/functions.rs @@ -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 Pallet { + // 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) + -> Option> + { + 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::Balance { + Account::::get(id, who.borrow()).balance + } + + /// Get the total supply of an asset `id`. + pub fn total_supply(id: T::AssetId) -> T::Balance { + Asset::::get(id).map(|x| x.supply).unwrap_or_else(Zero::zero) + } + + pub(super) fn new_account( + who: &T::AccountId, + d: &mut AssetDetails>, + ) -> Result { + let accounts = d.accounts.checked_add(1).ok_or(Error::::Overflow)?; + let is_sufficient = if d.is_sufficient { + frame_system::Pallet::::inc_sufficients(who); + d.sufficients += 1; + true + } else { + frame_system::Pallet::::inc_consumers(who).map_err(|_| Error::::NoProvider)?; + false + }; + d.accounts = accounts; + Ok(is_sufficient) + } + + pub(super) fn dead_account( + what: T::AssetId, + who: &T::AccountId, + d: &mut AssetDetails>, + sufficient: bool, + ) { + if sufficient { + d.sufficients = d.sufficients.saturating_sub(1); + frame_system::Pallet::::dec_sufficients(who); + } else { + frame_system::Pallet::::dec_consumers(who); + } + d.accounts = d.accounts.saturating_sub(1); + T::Freezer::died(what, who) + } + + pub(super) fn can_increase(id: T::AssetId, who: &T::AccountId, amount: T::Balance) -> DepositConsequence { + let details = match Asset::::get(id) { + Some(details) => details, + None => return DepositConsequence::UnknownAsset, + }; + if details.supply.checked_add(&amount).is_none() { + return DepositConsequence::Overflow + } + let account = Account::::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::::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 { + use WithdrawConsequence::*; + let details = match Asset::::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::::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::::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> { + let details = match Asset::::get(id) { + Some(details) => details, + None => return Err(Error::::Unknown), + }; + ensure!(!details.is_frozen, Error::::Frozen); + + let account = Account::::get(id, who); + ensure!(!account.is_frozen, Error::::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::::Overflow)?; + account.balance.saturating_sub(required) + } else { + let is_provider = false; + let is_required = is_provider && !frame_system::Pallet::::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 { + let actual = Self::reducible_balance(id, target, f.keep_alive)? + .min(amount); + ensure!(f.best_effort || actual >= amount, Error::::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), 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, + ) -> DispatchResult { + Self::increase_balance(id, beneficiary, amount, |details| -> DispatchResult { + if let Some(check_issuer) = maybe_check_issuer { + ensure!(&check_issuer == &details.issuer, Error::::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>) -> DispatchResult, + ) -> DispatchResult { + if amount.is_zero() { return Ok(()) } + + Self::can_increase(id, beneficiary, amount).into_result()?; + Asset::::try_mutate(id, |maybe_details| -> DispatchResult { + let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; + + check(details)?; + + Account::::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, + f: DebitFlags, + ) -> Result { + 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::::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>, + ) -> DispatchResult, + ) -> Result { + if amount.is_zero() { return Ok(amount) } + + let actual = Self::prep_debit(id, target, amount, f)?; + + Asset::::try_mutate(id, |maybe_details| -> DispatchResult { + let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; + + check(actual, details)?; + + Account::::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, + f: TransferFlags, + ) -> Result { + // 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::::get(id, &source); + + Asset::::try_mutate(id, |maybe_details| -> DispatchResult { + let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; + + // Check admin rights. + if let Some(need_admin) = maybe_need_admin { + ensure!(&need_admin == &details.admin, Error::::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::::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::::remove(id, &source); + } else { + Account::::insert(id, &source, &source_account) + } + + Ok(()) + })?; + + Self::deposit_event(Event::Transferred(id, source.clone(), dest.clone(), credit)); + Ok(credit) + } +} diff --git a/substrate/frame/assets/src/impl_fungibles.rs b/substrate/frame/assets/src/impl_fungibles.rs new file mode 100644 index 0000000000..a4cff9b7e9 --- /dev/null +++ b/substrate/frame/assets/src/impl_fungibles.rs @@ -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 fungibles::Inspect<::AccountId> for Pallet { + type AssetId = T::AssetId; + type Balance = T::Balance; + + fn total_issuance(asset: Self::AssetId) -> Self::Balance { + Asset::::get(asset).map(|x| x.supply).unwrap_or_else(Zero::zero) + } + + fn minimum_balance(asset: Self::AssetId) -> Self::Balance { + Asset::::get(asset).map(|x| x.min_balance).unwrap_or_else(Zero::zero) + } + + fn balance( + asset: Self::AssetId, + who: &::AccountId, + ) -> Self::Balance { + Pallet::::balance(asset, who) + } + + fn reducible_balance( + asset: Self::AssetId, + who: &::AccountId, + keep_alive: bool, + ) -> Self::Balance { + Pallet::::reducible_balance(asset, who, keep_alive).unwrap_or(Zero::zero()) + } + + fn can_deposit( + asset: Self::AssetId, + who: &::AccountId, + amount: Self::Balance, + ) -> DepositConsequence { + Pallet::::can_increase(asset, who, amount) + } + + fn can_withdraw( + asset: Self::AssetId, + who: &::AccountId, + amount: Self::Balance, + ) -> WithdrawConsequence { + Pallet::::can_decrease(asset, who, amount, false) + } +} + +impl fungibles::Mutate<::AccountId> for Pallet { + fn mint_into( + asset: Self::AssetId, + who: &::AccountId, + amount: Self::Balance, + ) -> DispatchResult { + Self::do_mint(asset, who, amount, None) + } + + fn burn_from( + asset: Self::AssetId, + who: &::AccountId, + amount: Self::Balance, + ) -> Result { + let f = DebitFlags { + keep_alive: false, + best_effort: false, + }; + Self::do_burn(asset, who, amount, None, f) + } + + fn slash( + asset: Self::AssetId, + who: &::AccountId, + amount: Self::Balance, + ) -> Result { + let f = DebitFlags { + keep_alive: false, + best_effort: true, + }; + Self::do_burn(asset, who, amount, None, f) + } +} + +impl fungibles::Transfer for Pallet { + fn transfer( + asset: Self::AssetId, + source: &T::AccountId, + dest: &T::AccountId, + amount: T::Balance, + keep_alive: bool, + ) -> Result { + let f = TransferFlags { + keep_alive, + best_effort: false, + burn_dust: false + }; + Self::do_transfer(asset, source, dest, amount, None, f) + } +} + +impl fungibles::Unbalanced for Pallet { + 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::::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 + { + 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::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(), + } + } +} diff --git a/substrate/frame/assets/src/impl_stored_map.rs b/substrate/frame/assets/src/impl_stored_map.rs new file mode 100644 index 0000000000..a8a6f95557 --- /dev/null +++ b/substrate/frame/assets/src/impl_stored_map.rs @@ -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 StoredMap<(T::AssetId, T::AccountId), T::Extra> for Pallet { + fn get(id_who: &(T::AssetId, T::AccountId)) -> T::Extra { + let &(id, ref who) = id_who; + if Account::::contains_key(id, who) { + Account::::get(id, who).extra + } else { + Default::default() + } + } + + fn try_mutate_exists>( + id_who: &(T::AssetId, T::AccountId), + f: impl FnOnce(&mut Option) -> Result, + ) -> Result { + let &(id, ref who) = id_who; + let mut maybe_extra = Some(Account::::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::::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) + }) + } +} diff --git a/substrate/frame/assets/src/lib.rs b/substrate/frame/assets/src/lib.rs index db7338e36e..2a162c2c93 100644 --- a/substrate/frame/assets/src/lib.rs +++ b/substrate/frame/assets/src/lib.rs @@ -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 fungibles::Inspect<::AccountId> for Pallet { - type AssetId = T::AssetId; - type Balance = T::Balance; - - fn total_issuance(asset: Self::AssetId) -> Self::Balance { - Asset::::get(asset).map(|x| x.supply).unwrap_or_else(Zero::zero) - } - - fn minimum_balance(asset: Self::AssetId) -> Self::Balance { - Asset::::get(asset).map(|x| x.min_balance).unwrap_or_else(Zero::zero) - } - - fn balance( - asset: Self::AssetId, - who: &::AccountId, - ) -> Self::Balance { - Pallet::::balance(asset, who) - } - - fn can_deposit( - asset: Self::AssetId, - who: &::AccountId, - amount: Self::Balance, - ) -> DepositConsequence { - Pallet::::can_deposit(asset, who, amount) - } - - fn can_withdraw( - asset: Self::AssetId, - who: &::AccountId, - amount: Self::Balance, - ) -> WithdrawConsequence { - Pallet::::can_withdraw(asset, who, amount) - } -} - -impl fungibles::Mutate<::AccountId> for Pallet { - fn deposit( - asset: Self::AssetId, - who: &::AccountId, - amount: Self::Balance, - ) -> DispatchResult { - Pallet::::increase_balance(asset, who.clone(), amount, None) - } - - fn withdraw( - asset: Self::AssetId, - who: &::AccountId, - amount: Self::Balance, - ) -> Result { - Pallet::::reduce_balance(asset, who.clone(), amount, None) - } -} - -impl fungibles::Transfer for Pallet { - fn transfer( - asset: Self::AssetId, - source: &T::AccountId, - dest: &T::AccountId, - amount: T::Balance, - ) -> Result { - >::transfer(asset, source, dest, amount) - } -} - -type DepositBalanceOf = <::Currency as Currency<::AccountId>>::Balance; - -#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug)] -pub struct AssetDetails< - Balance, - AccountId, - DepositBalance, -> { - /// Can change `owner`, `issuer`, `freezer` and `admin` accounts. - owner: AccountId, - /// Can mint tokens. - issuer: AccountId, - /// Can thaw tokens, force transfers and burn tokens from any account. - admin: AccountId, - /// Can freeze tokens. - freezer: AccountId, - /// The total supply across all accounts. - supply: Balance, - /// The balance deposited for this asset. This pays for the data stored here. - deposit: DepositBalance, - /// The ED for virtual accounts. - min_balance: Balance, - /// If `true`, then any account with this asset is given a provider reference. Otherwise, it - /// requires a consumer reference. - is_sufficient: bool, - /// The total number of accounts. - accounts: u32, - /// The total number of accounts for which we have placed a self-sufficient reference. - sufficients: u32, - /// The total number of approvals. - approvals: u32, - /// Whether the asset is frozen for non-admin transfers. - is_frozen: bool, -} - -impl AssetDetails { - pub fn destroy_witness(&self) -> DestroyWitness { - DestroyWitness { - accounts: self.accounts, - sufficients: self.sufficients, - approvals: self.approvals, - } - } -} - -/// A pair to act as a key for the approval storage map. -#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug)] -pub struct ApprovalKey { - /// The owner of the funds that are being approved. - owner: AccountId, - /// The party to whom transfer of the funds is being delegated. - delegate: AccountId, -} - -/// Data concerning an approval. -#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default)] -pub struct Approval { - /// The amount of funds approved for the balance transfer from the owner to some delegated - /// target. - amount: Balance, - /// The amount reserved on the owner's account to hold this item in storage. - deposit: DepositBalance, -} - -#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default)] -pub struct AssetBalance { - /// The balance. - balance: Balance, - /// Whether the account is frozen. - is_frozen: bool, - /// `true` if this balance gave the account a self-sufficient reference. - sufficient: bool, -} - -#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default)] -pub struct AssetMetadata { - /// The balance deposited for this metadata. - /// - /// This pays for the data stored in this struct. - deposit: DepositBalance, - /// The user friendly name of this asset. Limited in length by `StringLimit`. - name: Vec, - /// The ticker symbol for this asset. Limited in length by `StringLimit`. - symbol: Vec, - /// The number of decimals this asset uses to represent one unit. - decimals: u8, - /// Whether the asset metadata may be changed by a non Force origin. - is_frozen: bool, -} - -/// Witness data for the destroy transactions. -#[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug)] -pub struct DestroyWitness { - /// The number of accounts holding the asset. - #[codec(compact)] - accounts: u32, - /// The number of accounts holding the asset with a self-sufficient reference. - #[codec(compact)] - sufficients: u32, - /// The number of transfer-approvals of the asset. - #[codec(compact)] - approvals: u32, -} - #[frame_support::pallet] pub mod pallet { use frame_support::{ @@ -363,6 +202,13 @@ pub mod pallet { /// The maximum length of a name or symbol stored on-chain. type StringLimit: Get; + /// A hook to allow a per-asset, per-account minimum balance to be enforced. This must be + /// respected in all permissionless operations. + type Freezer: FrozenBalance; + + /// Additional data to be stored with an account's asset balance. + type Extra: Member + Parameter + Default; + /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; } @@ -384,7 +230,7 @@ pub mod pallet { T::AssetId, Blake2_128Concat, T::AccountId, - AssetBalance, + AssetBalance, ValueQuery, >; @@ -637,7 +483,7 @@ pub mod pallet { ensure!(details.approvals == witness.approvals, Error::::BadWitness); for (who, v) in Account::::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: ::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::::Unapproved)?; let remaining = approved.amount.checked_sub(&amount).ok_or(Error::::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 Pallet { - // Public immutables - - /// Get the asset `id` balance of `who`. - pub fn balance(id: T::AssetId, who: impl sp_std::borrow::Borrow) -> T::Balance { - Account::::get(id, who.borrow()).balance - } - - /// Get the total supply of an asset `id`. - pub fn total_supply(id: T::AssetId) -> T::Balance { - Asset::::get(id).map(|x| x.supply).unwrap_or_else(Zero::zero) - } - - fn new_account( - who: &T::AccountId, - d: &mut AssetDetails>, - ) -> Result { - let accounts = d.accounts.checked_add(1).ok_or(Error::::Overflow)?; - let is_sufficient = if d.is_sufficient { - frame_system::Pallet::::inc_sufficients(who); - d.sufficients += 1; - true - } else { - frame_system::Pallet::::inc_consumers(who).map_err(|_| Error::::NoProvider)?; - false - }; - d.accounts = accounts; - Ok(is_sufficient) - } - - fn dead_account( - who: &T::AccountId, - d: &mut AssetDetails>, - sufficient: bool, - ) { - if sufficient { - d.sufficients = d.sufficients.saturating_sub(1); - frame_system::Pallet::::dec_sufficients(who); - } else { - frame_system::Pallet::::dec_consumers(who); - } - d.accounts = d.accounts.saturating_sub(1); - } - - fn can_deposit(id: T::AssetId, who: &T::AccountId, amount: T::Balance) -> DepositConsequence { - let details = match Asset::::get(id) { - Some(details) => details, - None => return DepositConsequence::UnknownAsset, - }; - if details.supply.checked_add(&amount).is_none() { - return DepositConsequence::Overflow - } - let account = Account::::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::::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 { - let details = match Asset::::get(id) { - Some(details) => details, - None => return WithdrawConsequence::UnknownAsset, - }; - if details.supply.checked_sub(&amount).is_none() { - return WithdrawConsequence::Underflow - } - let account = Account::::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, - ) -> DispatchResult { - Asset::::try_mutate(id, |maybe_details| { - let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; - - if let Some(check_issuer) = maybe_check_issuer { - ensure!(&check_issuer == &details.issuer, Error::::NoPermission); - } - details.supply = details.supply.checked_add(&amount).ok_or(Error::::Overflow)?; - - Account::::try_mutate(id, &beneficiary, |t| -> DispatchResult { - let new_balance = t.balance.saturating_add(amount); - ensure!(new_balance >= details.min_balance, Error::::BalanceLow); - if t.balance.is_zero() { - t.sufficient = Self::new_account(&beneficiary, details)?; - } - t.balance = new_balance; - Ok(()) - })?; - Self::deposit_event(Event::Issued(id, beneficiary, amount)); - Ok(()) - }) - } - - fn reduce_balance( - id: T::AssetId, - target: T::AccountId, - amount: T::Balance, - maybe_check_admin: Option, - ) -> Result { - Asset::::try_mutate(id, |maybe_details| { - let d = maybe_details.as_mut().ok_or(Error::::Unknown)?; - if let Some(check_admin) = maybe_check_admin { - ensure!(&check_admin == &d.admin, Error::::NoPermission); - } - - let burned = Account::::try_mutate_exists( - id, - &target, - |maybe_account| -> Result { - let mut account = maybe_account.take().ok_or(Error::::BalanceZero)?; - let mut burned = amount.min(account.balance); - account.balance -= burned; - *maybe_account = if account.balance < d.min_balance { - burned += account.balance; - Self::dead_account(&target, d, account.sufficient); - None - } else { - Some(account) - }; - Ok(burned) - } - )?; - - d.supply = d.supply.saturating_sub(burned); - - Self::deposit_event(Event::Burned(id, target, burned)); - Ok(burned) - }) - } - - fn do_transfer( - id: T::AssetId, - source: T::AccountId, - dest: T::AccountId, - amount: T::Balance, - maybe_need_admin: Option, - keep_alive: bool, - ) -> DispatchResult { - let mut source_account = Account::::get(id, &source); - ensure!(!source_account.is_frozen, Error::::Frozen); - - source_account.balance = source_account.balance.checked_sub(&amount) - .ok_or(Error::::BalanceLow)?; - - Asset::::try_mutate(id, |maybe_details| { - let details = maybe_details.as_mut().ok_or(Error::::Unknown)?; - ensure!(!details.is_frozen, Error::::Frozen); - - if let Some(need_admin) = maybe_need_admin { - ensure!(&need_admin == &details.admin, Error::::NoPermission); - } - - if dest != source && !amount.is_zero() { - let mut amount = amount; - if source_account.balance < details.min_balance { - ensure!(!keep_alive, Error::::WouldDie); - amount += source_account.balance; - source_account.balance = Zero::zero(); - } - - Account::::try_mutate(id, &dest, |a| -> DispatchResult { - let new_balance = a.balance.saturating_add(amount); - - ensure!(new_balance >= details.min_balance, Error::::BalanceLow); - - if a.balance.is_zero() { - a.sufficient = Self::new_account(&dest, details)?; - } - a.balance = new_balance; - Ok(()) - })?; - - if source_account.balance.is_zero() { - Self::dead_account(&source, details, source_account.sufficient); - Account::::remove(id, &source); - } else { - Account::::insert(id, &source, &source_account) - } - } - - Self::deposit_event(Event::Transferred(id, source, dest, amount)); - Ok(()) - }) - } -} diff --git a/substrate/frame/assets/src/mock.rs b/substrate/frame/assets/src/mock.rs index 806d85ce71..26ff938512 100644 --- a/substrate/frame/assets/src/mock.rs +++ b/substrate/frame/assets/src/mock.rs @@ -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> = RefCell::new(Default::default()); + static HOOKS: RefCell> = RefCell::new(Default::default()); +} + +pub struct TestFreezer; +impl FrozenBalance for TestFreezer { + fn frozen_balance(asset: u32, who: &u64) -> Option { + 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 { + HOOKS.with(|h| h.borrow().clone()) } pub(crate) fn new_test_ext() -> sp_io::TestExternalities { diff --git a/substrate/frame/assets/src/tests.rs b/substrate/frame/assets/src/tests.rs index 1fe9358dcb..953164a0b9 100644 --- a/substrate/frame/assets/src/tests.rs +++ b/substrate/frame/assets/src/tests.rs @@ -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::::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::::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::::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::::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::::BalanceLow); - assert_noop!(Assets::transfer(Origin::signed(1), 0, 2, 9), Error::::BalanceLow); - assert_noop!(Assets::force_transfer(Origin::signed(1), 0, 1, 2, 9), Error::::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::::WouldDie); + assert_noop!(Assets::transfer_keep_alive(Origin::signed(1), 0, 2, 91), Error::::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::::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::::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::::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::::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); + }); +} diff --git a/substrate/frame/assets/src/types.rs b/substrate/frame/assets/src/types.rs new file mode 100644 index 0000000000..7e0e235b1b --- /dev/null +++ b/substrate/frame/assets/src/types.rs @@ -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 = <::Currency as Currency<::AccountId>>::Balance; + +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug)] +pub struct AssetDetails< + Balance, + AccountId, + DepositBalance, +> { + /// Can change `owner`, `issuer`, `freezer` and `admin` accounts. + 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 AssetDetails { + pub fn destroy_witness(&self) -> DestroyWitness { + DestroyWitness { + accounts: self.accounts, + sufficients: self.sufficients, + approvals: self.approvals, + } + } +} + +/// A pair to act as a key for the approval storage map. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug)] +pub struct ApprovalKey { + /// The owner of the funds that are being approved. + 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 { + /// 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 { + /// 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 { + /// 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, + /// The ticker symbol for this asset. Limited in length by `StringLimit`. + pub(super) symbol: Vec, + /// 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 { + /// 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; + + /// Called when an account has been removed. + fn died(asset: AssetId, who: &AccountId); +} + +impl FrozenBalance for () { + fn frozen_balance(_: AssetId, _: &AccountId) -> Option { 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 for DebitFlags { + fn from(f: TransferFlags) -> Self { + Self { + keep_alive: f.keep_alive, + best_effort: f.best_effort, + } + } +} diff --git a/substrate/frame/balances/src/lib.rs b/substrate/frame/balances/src/lib.rs index a2e858799b..35841c504a 100644 --- a/substrate/frame/balances/src/lib.rs +++ b/substrate/frame/balances/src/lib.rs @@ -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, I: 'static> Pallet { /// the caller will do this. pub fn mutate_account( who: &T::AccountId, - f: impl FnOnce(&mut AccountData) -> R + f: impl FnOnce(&mut AccountData) -> R, ) -> Result { Self::try_mutate_account(who, |a, _| -> Result { Ok(f(a)) }) } @@ -780,7 +781,7 @@ impl, I: 'static> Pallet { /// the caller will do this. fn try_mutate_account>( who: &T::AccountId, - f: impl FnOnce(&mut AccountData, bool) -> Result + f: impl FnOnce(&mut AccountData, bool) -> Result, ) -> Result { Self::try_mutate_account_with_dust(who, f) .map(|(result, dust_cleaner)| { @@ -804,7 +805,7 @@ impl, I: 'static> Pallet { /// the caller will do this. fn try_mutate_account_with_dust>( who: &T::AccountId, - f: impl FnOnce(&mut AccountData, bool) -> Result + f: impl FnOnce(&mut AccountData, bool) -> Result, ) -> Result<(R, DustCleaner), E> { let result = T::AccountStore::try_mutate_exists(who, |maybe_account| { let is_new = maybe_account.is_none(); @@ -873,9 +874,57 @@ impl, I: 'static> Pallet { } } } -} -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 { + 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), DispatchError> { + ensure!(!is_new, Error::::DeadAccount); + Self::try_mutate_account_with_dust( + slashed, + |from_account, _| -> Result { + let actual = cmp::min(from_account.reserved, value); + ensure!(best_effort || actual == value, Error::::InsufficientBalance); + match status { + Status::Free => to_account.free = to_account.free + .checked_add(&actual) + .ok_or(Error::::Overflow)?, + Status::Reserved => to_account.reserved = to_account.reserved + .checked_add(&actual) + .ok_or(Error::::Overflow)?, + } + from_account.reserved -= actual; + Ok(actual) + } + ) + } + )?; + + Self::deposit_event(Event::ReserveRepatriated(slashed.clone(), beneficiary.clone(), actual, status)); + Ok(actual) + } +} impl, I: 'static> fungible::Inspect for Pallet { type Balance = T::Balance; @@ -889,6 +938,19 @@ impl, I: 'static> fungible::Inspect for Pallet 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::::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, I: 'static> fungible::Inspect for Pallet } impl, I: 'static> fungible::Mutate for Pallet { - 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, I: 'static> fungible::Mutate for Pallet { Ok(()) } - fn withdraw(who: &T::AccountId, amount: Self::Balance) -> Result { + fn burn_from(who: &T::AccountId, amount: Self::Balance) -> Result { if amount.is_zero() { return Ok(Self::Balance::zero()); } - let actual = Self::try_mutate_account(who, |account, _is_new| -> Result { let extra = Self::withdraw_consequence(who, amount, &account).into_result()?; let actual = amount + extra; @@ -928,8 +989,11 @@ impl, I: 'static> fungible::Transfer for Pallet source: &T::AccountId, dest: &T::AccountId, amount: T::Balance, + keep_alive: bool, ) -> Result { - >::transfer(source, dest, amount) + let er = if keep_alive { KeepAlive } else { AllowDeath }; + >::transfer(source, dest, amount, er) + .map(|_| amount) } } @@ -944,6 +1008,60 @@ impl, I: 'static> fungible::Unbalanced for Pallet, I: 'static> fungible::InspectHold for Pallet { + 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, I: 'static> fungible::MutateHold for Pallet { + fn hold(who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + if amount.is_zero() { return Ok(()) } + ensure!(Self::can_reserve(who, amount), Error::::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 + { + 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::::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 { + 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, I: 'static> ReservableCurrency for Pallet value: Self::Balance, status: Status, ) -> Result { - 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), DispatchError> { - ensure!(!is_new, Error::::DeadAccount); - Self::try_mutate_account_with_dust( - slashed, - |from_account, _| -> Result { - let actual = cmp::min(from_account.reserved, value); - match status { - Status::Free => to_account.free = to_account.free - .checked_add(&actual) - .ok_or(Error::::Overflow)?, - Status::Reserved => to_account.reserved = to_account.reserved - .checked_add(&actual) - .ok_or(Error::::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)) } } diff --git a/substrate/frame/support/src/traits.rs b/substrate/frame/support/src/traits.rs index 391fa0b538..ba4869d4b8 100644 --- a/substrate/frame/support/src/traits.rs +++ b/substrate/frame/support/src/traits.rs @@ -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, }; diff --git a/substrate/frame/support/src/traits/tokens/fungible.rs b/substrate/frame/support/src/traits/tokens/fungible.rs index 8e6b4ace34..5472212aaa 100644 --- a/substrate/frame/support/src/traits/tokens/fungible.rs +++ b/substrate/frame/support/src/traits/tokens/fungible.rs @@ -32,14 +32,22 @@ pub use imbalance::{Imbalance, HandleImbalanceDrop, DebtOf, CreditOf}; pub trait Inspect { /// 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; @@ -47,26 +55,42 @@ pub trait Inspect { /// Trait for providing an ERC-20 style fungible asset. pub trait Mutate: Inspect { - /// 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; - /// 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; + + /// 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::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 { 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: Inspect { source: &AccountId, dest: &AccountId, amount: Self::Balance, + keep_alive: bool, ) -> Result; } -/// Trait for providing a fungible asset which can be reserved. -pub trait Reserve: Inspect { +/// Trait for inspecting a fungible asset which can be reserved. +pub trait InspectHold: Inspect { /// 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: InspectHold + Transfer { + /// 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; + + /// 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; +} + +/// Trait for slashing a fungible asset which can be reserved. +pub trait BalancedHold: Balanced + MutateHold { + /// 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, Self::Balance); +} + +impl< + AccountId, + T: Balanced + MutateHold, +> BalancedHold for T { + fn slash_held(who: &AccountId, amount: Self::Balance) + -> (CreditOf, Self::Balance) + { + let actual = match Self::release(who, amount, true) { + Ok(x) => x, + Err(_) => return (Imbalance::default(), amount), + }; + >::slash(who, actual) + } +} + +/// Convert a `fungibles` trait implementation into a `fungible` trait implementation by identifying +/// a single item. pub struct ItemOf< F: fungibles::Inspect, A: Get<>::AssetId>, @@ -129,6 +204,9 @@ impl< fn balance(who: &AccountId) -> Self::Balance { >::balance(A::get(), who) } + fn reducible_balance(who: &AccountId, keep_alive: bool) -> Self::Balance { + >::reducible_balance(A::get(), who, keep_alive) + } fn can_deposit(who: &AccountId, amount: Self::Balance) -> DepositConsequence { >::can_deposit(A::get(), who, amount) } @@ -142,11 +220,11 @@ impl< A: Get<>::AssetId>, AccountId, > Mutate for ItemOf { - fn deposit(who: &AccountId, amount: Self::Balance) -> DispatchResult { - >::deposit(A::get(), who, amount) + fn mint_into(who: &AccountId, amount: Self::Balance) -> DispatchResult { + >::mint_into(A::get(), who, amount) } - fn withdraw(who: &AccountId, amount: Self::Balance) -> Result { - >::withdraw(A::get(), who, amount) + fn burn_from(who: &AccountId, amount: Self::Balance) -> Result { + >::burn_from(A::get(), who, amount) } } @@ -155,39 +233,54 @@ impl< A: Get<>::AssetId>, AccountId, > Transfer for ItemOf { - fn transfer(source: &AccountId, dest: &AccountId, amount: Self::Balance) + fn transfer(source: &AccountId, dest: &AccountId, amount: Self::Balance, keep_alive: bool) -> Result { - >::transfer(A::get(), source, dest, amount) + >::transfer(A::get(), source, dest, amount, keep_alive) } } impl< - F: fungibles::Reserve, + F: fungibles::InspectHold, A: Get<>::AssetId>, AccountId, -> Reserve for ItemOf { - fn reserved_balance(who: &AccountId) -> Self::Balance { - >::reserved_balance(A::get(), who) +> InspectHold for ItemOf { + fn balance_on_hold(who: &AccountId) -> Self::Balance { + >::balance_on_hold(A::get(), who) } - fn total_balance(who: &AccountId) -> Self::Balance { - >::total_balance(A::get(), who) + fn can_hold(who: &AccountId, amount: Self::Balance) -> bool { + >::can_hold(A::get(), who, amount) } - fn can_reserve(who: &AccountId, amount: Self::Balance) -> bool { - >::can_reserve(A::get(), who, amount) +} + +impl< + F: fungibles::MutateHold, + A: Get<>::AssetId>, + AccountId, +> MutateHold for ItemOf { + fn hold(who: &AccountId, amount: Self::Balance) -> DispatchResult { + >::hold(A::get(), who, amount) } - fn reserve(who: &AccountId, amount: Self::Balance) -> DispatchResult { - >::reserve(A::get(), who, amount) + fn release(who: &AccountId, amount: Self::Balance, best_effort: bool) + -> Result + { + >::release(A::get(), who, amount, best_effort) } - fn unreserve(who: &AccountId, amount: Self::Balance) -> DispatchResult { - >::unreserve(A::get(), who, amount) - } - fn repatriate_reserved( - who: &AccountId, + fn transfer_held( + source: &AccountId, + dest: &AccountId, amount: Self::Balance, - status: BalanceStatus, - ) -> DispatchResult { - >::repatriate_reserved(A::get(), who, amount, status) + best_effort: bool, + on_hold: bool, + ) -> Result { + >::transfer_held( + A::get(), + source, + dest, + amount, + best_effort, + on_hold, + ) } } @@ -215,4 +308,3 @@ impl< >::increase_balance_at_most(A::get(), who, amount) } } - diff --git a/substrate/frame/support/src/traits/tokens/fungible/balanced.rs b/substrate/frame/support/src/traits/tokens/fungible/balanced.rs index 514a6f4c18..19bdb4f245 100644 --- a/substrate/frame/support/src/traits/tokens/fungible/balanced.rs +++ b/substrate/frame/support/src/traits/tokens/fungible/balanced.rs @@ -55,14 +55,11 @@ pub trait Balanced: Inspect { /// /// 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, CreditOf) - { + fn pair(amount: Self::Balance) -> (DebtOf, CreditOf) { (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. /// diff --git a/substrate/frame/support/src/traits/tokens/fungibles.rs b/substrate/frame/support/src/traits/tokens/fungibles.rs index 8f67798811..490f28dfb4 100644 --- a/substrate/frame/support/src/traits/tokens/fungibles.rs +++ b/substrate/frame/support/src/traits/tokens/fungibles.rs @@ -31,17 +31,26 @@ pub use imbalance::{Imbalance, HandleImbalanceDrop, DebtOf, CreditOf}; pub trait Inspect { /// 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: Inspect { /// /// 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: Inspect { /// 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; - /// 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::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: Inspect { ) -> Result { 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: Inspect { source: &AccountId, dest: &AccountId, amount: Self::Balance, + keep_alive: bool, ) -> Result; } -/// Trait for providing a set of named fungible assets which can be reserved. -pub trait Reserve: Inspect { - /// 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: Inspect { + /// 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: InspectHold + Transfer { + /// 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; + + /// 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; +} + +/// Trait for mutating one of several types of fungible assets which can be held. +pub trait BalancedHold: Balanced + MutateHold { + /// 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, Self::Balance); +} + +impl< + AccountId, + T: Balanced + MutateHold, +> BalancedHold for T { + fn slash_held(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) + -> (CreditOf, Self::Balance) + { + let actual = match Self::release(asset, who, amount, true) { + Ok(x) => x, + Err(_) => return (Imbalance::zero(asset), amount), + }; + >::slash(asset, who, actual) + } } diff --git a/substrate/frame/support/src/traits/tokens/fungibles/balanced.rs b/substrate/frame/support/src/traits/tokens/fungibles/balanced.rs index 0af07228e0..efb21300bc 100644 --- a/substrate/frame/support/src/traits/tokens/fungibles/balanced.rs +++ b/substrate/frame/support/src/traits/tokens/fungibles/balanced.rs @@ -30,7 +30,10 @@ use crate::traits::misc::{SameOrOther, TryDrop}; /// /// This is auto-implemented when a token class has `Unbalanced` implemented. pub trait Balanced: Inspect { + /// The type for managing what happens when an instance of `Debt` is dropped without being used. type OnDropDebt: HandleImbalanceDrop; + /// The type for managing what happens when an instance of `Credit` is dropped without being + /// used. type OnDropCredit: HandleImbalanceDrop; /// Reduce the total issuance by `amount` and return the according imbalance. The imbalance will diff --git a/substrate/frame/support/src/traits/tokens/misc.rs b/substrate/frame/support/src/traits/tokens/misc.rs index 303d183cf2..02f7ba384b 100644 --- a/substrate/frame/support/src/traits/tokens/misc.rs +++ b/substrate/frame/support/src/traits/tokens/misc.rs @@ -37,6 +37,9 @@ pub enum WithdrawConsequence { /// 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 WithdrawConsequence { 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()),