From 68fddd184423dc7def1735f2b88e8b21acabcd42 Mon Sep 17 00:00:00 2001 From: wangjj9219 <183318287@qq.com> Date: Wed, 14 Jun 2023 09:25:06 +0800 Subject: [PATCH 01/11] refactor fungibles impl --- tokens/src/currency_adapter.rs | 483 +++++++++ tokens/src/imbalances.rs | 174 ---- tokens/src/impl_currency.rs | 808 +++++++++++++++ tokens/src/impl_fungibles.rs | 349 +++++++ tokens/src/lib.rs | 1713 ++++---------------------------- 5 files changed, 1811 insertions(+), 1716 deletions(-) create mode 100644 tokens/src/currency_adapter.rs delete mode 100644 tokens/src/imbalances.rs create mode 100644 tokens/src/impl_currency.rs create mode 100644 tokens/src/impl_fungibles.rs diff --git a/tokens/src/currency_adapter.rs b/tokens/src/currency_adapter.rs new file mode 100644 index 000000000..7b4aaf167 --- /dev/null +++ b/tokens/src/currency_adapter.rs @@ -0,0 +1,483 @@ +//! The adapter for specific token, which implements the +//! frame_support::traits::Currency traits, orml Currency traits and fungible +//! traits. + +use super::*; + +pub struct CurrencyAdapter(marker::PhantomData<(T, GetCurrencyId)>); + +impl PalletCurrency for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type Balance = T::Balance; + type PositiveImbalance = PositiveImbalance; + type NegativeImbalance = NegativeImbalance; + + fn total_balance(who: &T::AccountId) -> Self::Balance { + as MultiCurrency<_>>::total_balance(GetCurrencyId::get(), who) + } + + fn can_slash(who: &T::AccountId, value: Self::Balance) -> bool { + as MultiCurrency<_>>::can_slash(GetCurrencyId::get(), who, value) + } + + fn total_issuance() -> Self::Balance { + as MultiCurrency<_>>::total_issuance(GetCurrencyId::get()) + } + + fn minimum_balance() -> Self::Balance { + as MultiCurrency<_>>::minimum_balance(GetCurrencyId::get()) + } + + fn burn(mut amount: Self::Balance) -> Self::PositiveImbalance { + if amount.is_zero() { + return PositiveImbalance::zero(); + } + let currency_id = GetCurrencyId::get(); + TotalIssuance::::mutate(currency_id, |issued| { + *issued = issued.checked_sub(&amount).unwrap_or_else(|| { + amount = *issued; + Zero::zero() + }) + }); + + Pallet::::deposit_event(Event::TotalIssuanceSet { + currency_id, + amount: Self::total_issuance(), + }); + PositiveImbalance::new(amount) + } + + fn issue(mut amount: Self::Balance) -> Self::NegativeImbalance { + if amount.is_zero() { + return NegativeImbalance::zero(); + } + TotalIssuance::::mutate(GetCurrencyId::get(), |issued| { + *issued = issued.checked_add(&amount).unwrap_or_else(|| { + amount = Self::Balance::max_value().defensive_saturating_sub(*issued); + Self::Balance::max_value() + }) + }); + + Pallet::::deposit_event(Event::TotalIssuanceSet { + currency_id: GetCurrencyId::get(), + amount: Self::total_issuance(), + }); + NegativeImbalance::new(amount) + } + + fn free_balance(who: &T::AccountId) -> Self::Balance { + as MultiCurrency<_>>::free_balance(GetCurrencyId::get(), who) + } + + fn ensure_can_withdraw( + who: &T::AccountId, + amount: Self::Balance, + _reasons: WithdrawReasons, + _new_balance: Self::Balance, + ) -> DispatchResult { + as MultiCurrency<_>>::ensure_can_withdraw(GetCurrencyId::get(), who, amount) + } + + fn transfer( + source: &T::AccountId, + dest: &T::AccountId, + value: Self::Balance, + existence_requirement: ExistenceRequirement, + ) -> DispatchResult { + Pallet::::do_transfer(GetCurrencyId::get(), source, dest, value, existence_requirement) + } + + fn slash(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { + if value.is_zero() { + return (Self::NegativeImbalance::zero(), value); + } + + let currency_id = GetCurrencyId::get(); + let account = Pallet::::accounts(who, currency_id); + let free_slashed_amount = account.free.min(value); + let mut remaining_slash = value.defensive_saturating_sub(free_slashed_amount); + + // slash free balance + if !free_slashed_amount.is_zero() { + Pallet::::set_free_balance( + currency_id, + who, + account.free.defensive_saturating_sub(free_slashed_amount), + ); + } + + // slash reserved balance + if !remaining_slash.is_zero() { + let reserved_slashed_amount = account.reserved.min(remaining_slash); + remaining_slash = remaining_slash.defensive_saturating_sub(reserved_slashed_amount); + Pallet::::set_reserved_balance( + currency_id, + who, + account.reserved.defensive_saturating_sub(reserved_slashed_amount), + ); + + Pallet::::deposit_event(Event::Slashed { + currency_id, + who: who.clone(), + free_amount: free_slashed_amount, + reserved_amount: reserved_slashed_amount, + }); + ( + Self::NegativeImbalance::new(free_slashed_amount.saturating_add(reserved_slashed_amount)), + remaining_slash, + ) + } else { + Pallet::::deposit_event(Event::Slashed { + currency_id, + who: who.clone(), + free_amount: value, + reserved_amount: Zero::zero(), + }); + (Self::NegativeImbalance::new(value), remaining_slash) + } + } + + /// Deposit some `value` into the free balance of an existing target account + /// `who`. + fn deposit_into_existing( + who: &T::AccountId, + value: Self::Balance, + ) -> sp_std::result::Result { + // do not change total issuance + Pallet::::do_deposit(GetCurrencyId::get(), who, value, true, false).map(|_| PositiveImbalance::new(value)) + } + + /// Deposit some `value` into the free balance of `who`, possibly creating a + /// new account. + fn deposit_creating(who: &T::AccountId, value: Self::Balance) -> Self::PositiveImbalance { + // do not change total issuance + Pallet::::do_deposit(GetCurrencyId::get(), who, value, false, false) + .map_or_else(|_| Self::PositiveImbalance::zero(), |_| PositiveImbalance::new(value)) + } + + fn withdraw( + who: &T::AccountId, + value: Self::Balance, + _reasons: WithdrawReasons, + liveness: ExistenceRequirement, + ) -> sp_std::result::Result { + // do not change total issuance + Pallet::::do_withdraw(GetCurrencyId::get(), who, value, liveness, false) + .map(|_| Self::NegativeImbalance::new(value)) + } + + fn make_free_balance_be( + who: &T::AccountId, + value: Self::Balance, + ) -> SignedImbalance { + let currency_id = GetCurrencyId::get(); + Pallet::::try_mutate_account( + currency_id, + who, + |account, is_new| -> Result, ()> { + // If we're attempting to set an existing account to less than ED, then + // bypass the entire operation. It's a no-op if you follow it through, but + // since this is an instance where we might account for a negative imbalance + // (in the dust cleaner of set_account) before we account for its actual + // equal and opposite cause (returned as an Imbalance), then in the + // instance that there's no other accounts on the system at all, we might + // underflow the issuance and our arithmetic will be off. + let ed = T::ExistentialDeposits::get(¤cy_id); + ensure!(value.saturating_add(account.reserved) >= ed || !is_new, ()); + + let imbalance = if account.free <= value { + SignedImbalance::Positive(PositiveImbalance::new(value.saturating_sub(account.free))) + } else { + SignedImbalance::Negative(NegativeImbalance::new(account.free.saturating_sub(value))) + }; + account.free = value; + + Pallet::::deposit_event(Event::BalanceSet { + currency_id, + who: who.clone(), + free: value, + reserved: account.reserved, + }); + Ok(imbalance) + }, + ) + .map(|(imbalance, _)| imbalance) + .unwrap_or_else(|_| SignedImbalance::Positive(Self::PositiveImbalance::zero())) + } +} + +impl PalletReservableCurrency for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn can_reserve(who: &T::AccountId, value: Self::Balance) -> bool { + as MultiReservableCurrency<_>>::can_reserve(GetCurrencyId::get(), who, value) + } + + fn slash_reserved(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { + let actual = as MultiReservableCurrency<_>>::slash_reserved(GetCurrencyId::get(), who, value); + (Self::NegativeImbalance::zero(), actual) + } + + fn reserved_balance(who: &T::AccountId) -> Self::Balance { + as MultiReservableCurrency<_>>::reserved_balance(GetCurrencyId::get(), who) + } + + fn reserve(who: &T::AccountId, value: Self::Balance) -> DispatchResult { + as MultiReservableCurrency<_>>::reserve(GetCurrencyId::get(), who, value) + } + + fn unreserve(who: &T::AccountId, value: Self::Balance) -> Self::Balance { + as MultiReservableCurrency<_>>::unreserve(GetCurrencyId::get(), who, value) + } + + fn repatriate_reserved( + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: Status, + ) -> sp_std::result::Result { + as MultiReservableCurrency<_>>::repatriate_reserved( + GetCurrencyId::get(), + slashed, + beneficiary, + value, + status, + ) + } +} + +impl PalletNamedReservableCurrency for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type ReserveIdentifier = T::ReserveIdentifier; + + fn reserved_balance_named(id: &Self::ReserveIdentifier, who: &T::AccountId) -> Self::Balance { + as NamedMultiReservableCurrency<_>>::reserved_balance_named(id, GetCurrencyId::get(), who) + } + + fn reserve_named(id: &Self::ReserveIdentifier, who: &T::AccountId, value: Self::Balance) -> DispatchResult { + as NamedMultiReservableCurrency<_>>::reserve_named(id, GetCurrencyId::get(), who, value) + } + + fn unreserve_named(id: &Self::ReserveIdentifier, who: &T::AccountId, value: Self::Balance) -> Self::Balance { + as NamedMultiReservableCurrency<_>>::unreserve_named(id, GetCurrencyId::get(), who, value) + } + + fn slash_reserved_named( + id: &Self::ReserveIdentifier, + who: &T::AccountId, + value: Self::Balance, + ) -> (Self::NegativeImbalance, Self::Balance) { + let actual = + as NamedMultiReservableCurrency<_>>::slash_reserved_named(id, GetCurrencyId::get(), who, value); + (Self::NegativeImbalance::zero(), actual) + } + + fn repatriate_reserved_named( + id: &Self::ReserveIdentifier, + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: Status, + ) -> sp_std::result::Result { + as NamedMultiReservableCurrency<_>>::repatriate_reserved_named( + id, + GetCurrencyId::get(), + slashed, + beneficiary, + value, + status, + ) + } +} + +impl PalletLockableCurrency for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type Moment = T::BlockNumber; + type MaxLocks = (); + + fn set_lock(id: LockIdentifier, who: &T::AccountId, amount: Self::Balance, _reasons: WithdrawReasons) { + let _ = as MultiLockableCurrency<_>>::set_lock(id, GetCurrencyId::get(), who, amount); + } + + fn extend_lock(id: LockIdentifier, who: &T::AccountId, amount: Self::Balance, _reasons: WithdrawReasons) { + let _ = as MultiLockableCurrency<_>>::extend_lock(id, GetCurrencyId::get(), who, amount); + } + + fn remove_lock(id: LockIdentifier, who: &T::AccountId) { + let _ = as MultiLockableCurrency<_>>::remove_lock(id, GetCurrencyId::get(), who); + } +} + +impl TransferAll for Pallet { + #[transactional] + fn transfer_all(source: &T::AccountId, dest: &T::AccountId) -> DispatchResult { + Accounts::::iter_prefix(source).try_for_each(|(currency_id, account_data)| -> DispatchResult { + // allow death + Self::do_transfer( + currency_id, + source, + dest, + account_data.free, + ExistenceRequirement::AllowDeath, + ) + }) + } +} + +impl fungible::Inspect for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type Balance = T::Balance; + + fn total_issuance() -> Self::Balance { + as fungibles::Inspect<_>>::total_issuance(GetCurrencyId::get()) + } + fn minimum_balance() -> Self::Balance { + as fungibles::Inspect<_>>::minimum_balance(GetCurrencyId::get()) + } + fn balance(who: &T::AccountId) -> Self::Balance { + as fungibles::Inspect<_>>::balance(GetCurrencyId::get(), who) + } + fn total_balance(who: &T::AccountId) -> Self::Balance { + as fungibles::Inspect<_>>::total_balance(GetCurrencyId::get(), who) + } + fn reducible_balance(who: &T::AccountId, preservation: Preservation, fortitude: Fortitude) -> Self::Balance { + as fungibles::Inspect<_>>::reducible_balance(GetCurrencyId::get(), who, preservation, fortitude) + } + fn can_deposit(who: &T::AccountId, amount: Self::Balance, provenance: Provenance) -> DepositConsequence { + as fungibles::Inspect<_>>::can_deposit(GetCurrencyId::get(), who, amount, provenance) + } + fn can_withdraw(who: &T::AccountId, amount: Self::Balance) -> WithdrawConsequence { + as fungibles::Inspect<_>>::can_withdraw(GetCurrencyId::get(), who, amount) + } +} + +impl fungible::Mutate for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn mint_into(who: &T::AccountId, amount: Self::Balance) -> Result { + as fungibles::Mutate<_>>::mint_into(GetCurrencyId::get(), who, amount) + } + fn burn_from( + who: &T::AccountId, + amount: Self::Balance, + precision: Precision, + fortitude: Fortitude, + ) -> Result { + as fungibles::Mutate<_>>::burn_from(GetCurrencyId::get(), who, amount, precision, fortitude) + } + + fn transfer( + source: &T::AccountId, + dest: &T::AccountId, + amount: T::Balance, + preservation: Preservation, + ) -> Result { + as fungibles::Mutate<_>>::transfer(GetCurrencyId::get(), source, dest, amount, preservation) + } +} + +impl fungible::Unbalanced for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn handle_dust(_dust: fungible::Dust) { + // Dust is handled in account mutate method + } + + fn write_balance(who: &T::AccountId, amount: Self::Balance) -> Result, DispatchError> { + as fungibles::Unbalanced<_>>::write_balance(GetCurrencyId::get(), who, amount) + } + fn set_total_issuance(amount: Self::Balance) { + as fungibles::Unbalanced<_>>::set_total_issuance(GetCurrencyId::get(), amount) + } +} + +type ReasonOfFungible =

::AccountId>>::Reason; +impl fungible::InspectHold for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type Reason = as fungibles::InspectHold>::Reason; + + fn balance_on_hold(reason: &Self::Reason, who: &T::AccountId) -> T::Balance { + as fungibles::InspectHold<_>>::balance_on_hold(GetCurrencyId::get(), reason, who) + } + fn total_balance_on_hold(who: &T::AccountId) -> Self::Balance { + as fungibles::InspectHold<_>>::total_balance_on_hold(GetCurrencyId::get(), who) + } + fn reducible_total_balance_on_hold(who: &T::AccountId, force: Fortitude) -> Self::Balance { + as fungibles::InspectHold<_>>::reducible_total_balance_on_hold(GetCurrencyId::get(), who, force) + } + fn hold_available(reason: &Self::Reason, who: &T::AccountId) -> bool { + as fungibles::InspectHold<_>>::hold_available(GetCurrencyId::get(), reason, who) + } + fn can_hold(reason: &Self::Reason, who: &T::AccountId, amount: T::Balance) -> bool { + as fungibles::InspectHold<_>>::can_hold(GetCurrencyId::get(), reason, who, amount) + } +} + +impl fungible::MutateHold for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn hold(reason: &ReasonOfFungible, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + as fungibles::MutateHold<_>>::hold(GetCurrencyId::get(), reason, who, amount) + } + fn release( + reason: &ReasonOfFungible, + who: &T::AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + as fungibles::MutateHold<_>>::release(GetCurrencyId::get(), reason, who, amount, precision) + } + fn transfer_on_hold( + reason: &ReasonOfFungible, + source: &T::AccountId, + dest: &T::AccountId, + amount: Self::Balance, + precision: Precision, + restriction: Restriction, + fortitude: Fortitude, + ) -> Result { + as fungibles::MutateHold<_>>::transfer_on_hold( + GetCurrencyId::get(), + reason, + source, + dest, + amount, + precision, + restriction, + fortitude, + ) + } +} + +impl fungible::UnbalancedHold for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn set_balance_on_hold(reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + as fungibles::UnbalancedHold<_>>::set_balance_on_hold(GetCurrencyId::get(), reason, who, amount) + } +} diff --git a/tokens/src/imbalances.rs b/tokens/src/imbalances.rs deleted file mode 100644 index 6aef35cd5..000000000 --- a/tokens/src/imbalances.rs +++ /dev/null @@ -1,174 +0,0 @@ -// wrapping these imbalances in a private module is necessary to ensure absolute -// privacy of the inner member. -use crate::{Config, TotalIssuance}; -use frame_support::traits::{Get, Imbalance, SameOrOther, TryDrop}; -use sp_runtime::traits::{Saturating, Zero}; -use sp_std::{marker, mem, result}; - -/// Opaque, move-only struct with private fields that serves as a token -/// denoting that funds have been created without any equal and opposite -/// accounting. -#[must_use] -pub struct PositiveImbalance>( - T::Balance, - marker::PhantomData, -); - -impl> PositiveImbalance { - /// Create a new positive imbalance from a balance. - pub fn new(amount: T::Balance) -> Self { - PositiveImbalance(amount, marker::PhantomData::) - } -} - -impl> Default for PositiveImbalance { - fn default() -> Self { - Self::zero() - } -} - -/// Opaque, move-only struct with private fields that serves as a token -/// denoting that funds have been destroyed without any equal and opposite -/// accounting. -#[must_use] -pub struct NegativeImbalance>( - T::Balance, - marker::PhantomData, -); - -impl> NegativeImbalance { - /// Create a new negative imbalance from a balance. - pub fn new(amount: T::Balance) -> Self { - NegativeImbalance(amount, marker::PhantomData::) - } -} - -impl> Default for NegativeImbalance { - fn default() -> Self { - Self::zero() - } -} - -impl> TryDrop for PositiveImbalance { - fn try_drop(self) -> result::Result<(), Self> { - self.drop_zero() - } -} - -impl> Imbalance for PositiveImbalance { - type Opposite = NegativeImbalance; - - fn zero() -> Self { - Self::new(Zero::zero()) - } - fn drop_zero(self) -> result::Result<(), Self> { - if self.0.is_zero() { - Ok(()) - } else { - Err(self) - } - } - fn split(self, amount: T::Balance) -> (Self, Self) { - let first = self.0.min(amount); - let second = self.0.saturating_sub(first); - - mem::forget(self); - (Self::new(first), Self::new(second)) - } - fn merge(mut self, other: Self) -> Self { - self.0 = self.0.saturating_add(other.0); - mem::forget(other); - - self - } - fn subsume(&mut self, other: Self) { - self.0 = self.0.saturating_add(other.0); - mem::forget(other); - } - // allow to make the impl same with `pallet-balances` - #[allow(clippy::comparison_chain)] - fn offset(self, other: Self::Opposite) -> SameOrOther { - let (a, b) = (self.0, other.0); - mem::forget((self, other)); - - if a > b { - SameOrOther::Same(Self::new(a.saturating_sub(b))) - } else if b > a { - SameOrOther::Other(NegativeImbalance::new(b.saturating_sub(a))) - } else { - SameOrOther::None - } - } - fn peek(&self) -> T::Balance { - self.0 - } -} - -impl> TryDrop for NegativeImbalance { - fn try_drop(self) -> result::Result<(), Self> { - self.drop_zero() - } -} - -impl> Imbalance for NegativeImbalance { - type Opposite = PositiveImbalance; - - fn zero() -> Self { - Self::new(Zero::zero()) - } - fn drop_zero(self) -> result::Result<(), Self> { - if self.0.is_zero() { - Ok(()) - } else { - Err(self) - } - } - fn split(self, amount: T::Balance) -> (Self, Self) { - let first = self.0.min(amount); - let second = self.0.saturating_sub(first); - - mem::forget(self); - (Self::new(first), Self::new(second)) - } - fn merge(mut self, other: Self) -> Self { - self.0 = self.0.saturating_add(other.0); - mem::forget(other); - - self - } - fn subsume(&mut self, other: Self) { - self.0 = self.0.saturating_add(other.0); - mem::forget(other); - } - // allow to make the impl same with `pallet-balances` - #[allow(clippy::comparison_chain)] - fn offset(self, other: Self::Opposite) -> SameOrOther { - let (a, b) = (self.0, other.0); - mem::forget((self, other)); - - if a > b { - SameOrOther::Same(Self::new(a.saturating_sub(b))) - } else if b > a { - SameOrOther::Other(PositiveImbalance::new(b.saturating_sub(a))) - } else { - SameOrOther::None - } - } - fn peek(&self) -> T::Balance { - self.0 - } -} - -impl> Drop for PositiveImbalance { - /// Basic drop handler will just square up the total issuance. - fn drop(&mut self) { - TotalIssuance::::mutate(GetCurrencyId::get(), |v| *v = v.saturating_add(self.0)); - } -} - -impl> Drop for NegativeImbalance { - /// Basic drop handler will just square up the total issuance. - fn drop(&mut self) { - TotalIssuance::::mutate(GetCurrencyId::get(), |v| *v = v.saturating_sub(self.0)); - } -} diff --git a/tokens/src/impl_currency.rs b/tokens/src/impl_currency.rs new file mode 100644 index 000000000..cbab5d4aa --- /dev/null +++ b/tokens/src/impl_currency.rs @@ -0,0 +1,808 @@ +// wrapping these imbalances in a private module is necessary to ensure absolute +// privacy of the inner member. +use super::*; +use frame_support::traits::{Get, Imbalance, SameOrOther, TryDrop}; +use sp_runtime::traits::{Saturating, Zero}; +use sp_std::{marker, mem, result}; + +/// Opaque, move-only struct with private fields that serves as a token +/// denoting that funds have been created without any equal and opposite +/// accounting. +#[must_use] +pub struct PositiveImbalance>( + T::Balance, + marker::PhantomData, +); + +impl> PositiveImbalance { + /// Create a new positive imbalance from a balance. + pub fn new(amount: T::Balance) -> Self { + PositiveImbalance(amount, marker::PhantomData::) + } +} + +impl> Default for PositiveImbalance { + fn default() -> Self { + Self::zero() + } +} + +/// Opaque, move-only struct with private fields that serves as a token +/// denoting that funds have been destroyed without any equal and opposite +/// accounting. +#[must_use] +pub struct NegativeImbalance>( + T::Balance, + marker::PhantomData, +); + +impl> NegativeImbalance { + /// Create a new negative imbalance from a balance. + pub fn new(amount: T::Balance) -> Self { + NegativeImbalance(amount, marker::PhantomData::) + } +} + +impl> Default for NegativeImbalance { + fn default() -> Self { + Self::zero() + } +} + +impl> TryDrop for PositiveImbalance { + fn try_drop(self) -> result::Result<(), Self> { + self.drop_zero() + } +} + +impl> Imbalance for PositiveImbalance { + type Opposite = NegativeImbalance; + + fn zero() -> Self { + Self::new(Zero::zero()) + } + fn drop_zero(self) -> result::Result<(), Self> { + if self.0.is_zero() { + Ok(()) + } else { + Err(self) + } + } + fn split(self, amount: T::Balance) -> (Self, Self) { + let first = self.0.min(amount); + let second = self.0.saturating_sub(first); + + mem::forget(self); + (Self::new(first), Self::new(second)) + } + fn merge(mut self, other: Self) -> Self { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + + self + } + fn subsume(&mut self, other: Self) { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + } + // allow to make the impl same with `pallet-balances` + #[allow(clippy::comparison_chain)] + fn offset(self, other: Self::Opposite) -> SameOrOther { + let (a, b) = (self.0, other.0); + mem::forget((self, other)); + + if a > b { + SameOrOther::Same(Self::new(a.saturating_sub(b))) + } else if b > a { + SameOrOther::Other(NegativeImbalance::new(b.saturating_sub(a))) + } else { + SameOrOther::None + } + } + fn peek(&self) -> T::Balance { + self.0 + } +} + +impl> TryDrop for NegativeImbalance { + fn try_drop(self) -> result::Result<(), Self> { + self.drop_zero() + } +} + +impl> Imbalance for NegativeImbalance { + type Opposite = PositiveImbalance; + + fn zero() -> Self { + Self::new(Zero::zero()) + } + fn drop_zero(self) -> result::Result<(), Self> { + if self.0.is_zero() { + Ok(()) + } else { + Err(self) + } + } + fn split(self, amount: T::Balance) -> (Self, Self) { + let first = self.0.min(amount); + let second = self.0.saturating_sub(first); + + mem::forget(self); + (Self::new(first), Self::new(second)) + } + fn merge(mut self, other: Self) -> Self { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + + self + } + fn subsume(&mut self, other: Self) { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + } + // allow to make the impl same with `pallet-balances` + #[allow(clippy::comparison_chain)] + fn offset(self, other: Self::Opposite) -> SameOrOther { + let (a, b) = (self.0, other.0); + mem::forget((self, other)); + + if a > b { + SameOrOther::Same(Self::new(a.saturating_sub(b))) + } else if b > a { + SameOrOther::Other(PositiveImbalance::new(b.saturating_sub(a))) + } else { + SameOrOther::None + } + } + fn peek(&self) -> T::Balance { + self.0 + } +} + +impl> Drop for PositiveImbalance { + /// Basic drop handler will just square up the total issuance. + fn drop(&mut self) { + TotalIssuance::::mutate(GetCurrencyId::get(), |v| *v = v.saturating_add(self.0)); + } +} + +impl> Drop for NegativeImbalance { + /// Basic drop handler will just square up the total issuance. + fn drop(&mut self) { + TotalIssuance::::mutate(GetCurrencyId::get(), |v| *v = v.saturating_sub(self.0)); + } +} + +/// Implementation of Currency traits for Tokens Module. +impl MultiCurrency for Pallet { + type CurrencyId = T::CurrencyId; + type Balance = T::Balance; + + fn minimum_balance(currency_id: Self::CurrencyId) -> Self::Balance { + Self::ed(currency_id) + } + + fn total_issuance(currency_id: Self::CurrencyId) -> Self::Balance { + Self::total_issuance(currency_id) + } + + fn total_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { + Self::accounts(who, currency_id).total() + } + + fn free_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { + Self::accounts(who, currency_id).free + } + + fn ensure_can_withdraw(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + Self::ensure_can_withdraw(currency_id, who, amount) + } + + fn transfer( + currency_id: Self::CurrencyId, + from: &T::AccountId, + to: &T::AccountId, + amount: Self::Balance, + ) -> DispatchResult { + // allow death + Self::do_transfer(currency_id, from, to, amount, ExistenceRequirement::AllowDeath) + } + + fn deposit(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + // do not require existing + // need change total issuance + Self::do_deposit(currency_id, who, amount, false, true)?; + Ok(()) + } + + fn withdraw(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + // allow death + // need change total issuance + Self::do_withdraw(currency_id, who, amount, ExistenceRequirement::AllowDeath, true) + } + + // Check if `value` amount of free balance can be slashed from `who`. + fn can_slash(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> bool { + if value.is_zero() { + return true; + } + Self::free_balance(currency_id, who) >= value + } + + /// Is a no-op if `value` to be slashed is zero. + /// + /// NOTE: `slash()` prefers free balance, but assumes that reserve + /// balance can be drawn from in extreme circumstances. `can_slash()` + /// should be used prior to `slash()` to avoid having to draw from + /// reserved funds, however we err on the side of punishment if things + /// are inconsistent or `can_slash` wasn't used appropriately. + fn slash(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> Self::Balance { + if amount.is_zero() { + return amount; + } + + >::OnSlash::on_slash( + currency_id, + who, + amount, + ); + let account = Self::accounts(who, currency_id); + let free_slashed_amount = account.free.min(amount); + // Cannot underflow because free_slashed_amount can never be greater than amount + // but just to be defensive here. + let mut remaining_slash = amount.defensive_saturating_sub(free_slashed_amount); + + // slash free balance + if !free_slashed_amount.is_zero() { + // Cannot underflow becuase free_slashed_amount can never be greater than + // account.free but just to be defensive here. + Self::set_free_balance( + currency_id, + who, + account.free.defensive_saturating_sub(free_slashed_amount), + ); + } + + // slash reserved balance + let reserved_slashed_amount = account.reserved.min(remaining_slash); + + if !reserved_slashed_amount.is_zero() { + // Cannot underflow due to above line but just to be defensive here. + remaining_slash = remaining_slash.defensive_saturating_sub(reserved_slashed_amount); + Self::set_reserved_balance( + currency_id, + who, + account.reserved.defensive_saturating_sub(reserved_slashed_amount), + ); + } + + // Cannot underflow because the slashed value cannot be greater than total + // issuance but just to be defensive here. + TotalIssuance::::mutate(currency_id, |v| { + *v = v.defensive_saturating_sub(amount.defensive_saturating_sub(remaining_slash)) + }); + + Self::deposit_event(Event::Slashed { + currency_id, + who: who.clone(), + free_amount: free_slashed_amount, + reserved_amount: reserved_slashed_amount, + }); + remaining_slash + } +} + +impl MultiCurrencyExtended for Pallet { + type Amount = T::Amount; + + fn update_balance(currency_id: Self::CurrencyId, who: &T::AccountId, by_amount: Self::Amount) -> DispatchResult { + if by_amount.is_zero() { + return Ok(()); + } + + // Ensure this doesn't overflow. There isn't any traits that exposes + // `saturating_abs` so we need to do it manually. + let by_amount_abs = if by_amount == Self::Amount::min_value() { + Self::Amount::max_value() + } else { + by_amount.abs() + }; + + let by_balance = + TryInto::::try_into(by_amount_abs).map_err(|_| Error::::AmountIntoBalanceFailed)?; + if by_amount.is_positive() { + Self::deposit(currency_id, who, by_balance) + } else { + Self::withdraw(currency_id, who, by_balance).map(|_| ()) + } + } +} + +impl MultiLockableCurrency for Pallet { + type Moment = T::BlockNumber; + + // Set a lock on the balance of `who` under `currency_id`. + // Is a no-op if lock amount is zero. + fn set_lock( + lock_id: LockIdentifier, + currency_id: Self::CurrencyId, + who: &T::AccountId, + amount: Self::Balance, + ) -> DispatchResult { + if amount.is_zero() { + return Ok(()); + } + let mut new_lock = Some(BalanceLock { id: lock_id, amount }); + let mut locks = Self::locks(who, currency_id) + .into_iter() + .filter_map(|lock| { + if lock.id == lock_id { + new_lock.take() + } else { + Some(lock) + } + }) + .collect::>(); + if let Some(lock) = new_lock { + locks.push(lock) + } + Self::update_locks(currency_id, who, &locks[..])?; + + Self::deposit_event(Event::LockSet { + lock_id, + currency_id, + who: who.clone(), + amount, + }); + Ok(()) + } + + // Extend a lock on the balance of `who` under `currency_id`. + // Is a no-op if lock amount is zero + fn extend_lock( + lock_id: LockIdentifier, + currency_id: Self::CurrencyId, + who: &T::AccountId, + amount: Self::Balance, + ) -> DispatchResult { + if amount.is_zero() { + return Ok(()); + } + let mut new_lock = Some(BalanceLock { id: lock_id, amount }); + let mut locks = Self::locks(who, currency_id) + .into_iter() + .filter_map(|lock| { + if lock.id == lock_id { + new_lock.take().map(|nl| BalanceLock { + id: lock.id, + amount: lock.amount.max(nl.amount), + }) + } else { + Some(lock) + } + }) + .collect::>(); + if let Some(lock) = new_lock { + locks.push(lock) + } + Self::update_locks(currency_id, who, &locks[..]) + } + + fn remove_lock(lock_id: LockIdentifier, currency_id: Self::CurrencyId, who: &T::AccountId) -> DispatchResult { + let mut locks = Self::locks(who, currency_id); + locks.retain(|lock| lock.id != lock_id); + let locks_vec = locks.to_vec(); + Self::update_locks(currency_id, who, &locks_vec[..])?; + + Self::deposit_event(Event::LockRemoved { + lock_id, + currency_id, + who: who.clone(), + }); + Ok(()) + } +} + +impl MultiReservableCurrency for Pallet { + /// Check if `who` can reserve `value` from their free balance. + /// + /// Always `true` if value to be reserved is zero. + fn can_reserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> bool { + if value.is_zero() { + return true; + } + Self::ensure_can_withdraw(currency_id, who, value).is_ok() + } + + /// Slash from reserved balance, returning any amount that was unable to + /// be slashed. + /// + /// Is a no-op if the value to be slashed is zero. + fn slash_reserved(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> Self::Balance { + if value.is_zero() { + return value; + } + + >::OnSlash::on_slash( + currency_id, + who, + value, + ); + let reserved_balance = Self::reserved_balance(currency_id, who); + let actual = reserved_balance.min(value); + Self::mutate_account(currency_id, who, |account| { + // ensured reserved_balance >= actual but just to be defensive here. + account.reserved = reserved_balance.defensive_saturating_sub(actual); + }); + TotalIssuance::::mutate(currency_id, |v| *v = v.defensive_saturating_sub(actual)); + + Self::deposit_event(Event::Slashed { + currency_id, + who: who.clone(), + free_amount: Zero::zero(), + reserved_amount: actual, + }); + value.defensive_saturating_sub(actual) + } + + fn reserved_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { + Self::accounts(who, currency_id).reserved + } + + /// Move `value` from the free balance from `who` to their reserved + /// balance. + /// + /// Is a no-op if value to be reserved is zero. + fn reserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> DispatchResult { + if value.is_zero() { + return Ok(()); + } + Self::ensure_can_withdraw(currency_id, who, value)?; + + Self::mutate_account(currency_id, who, |account| { + account.free = account.free.defensive_saturating_sub(value); + account.reserved = account.reserved.defensive_saturating_add(value); + + Self::deposit_event(Event::Reserved { + currency_id, + who: who.clone(), + amount: value, + }); + }); + + Ok(()) + } + + /// Unreserve some funds, returning any amount that was unable to be + /// unreserved. + /// + /// Is a no-op if the value to be unreserved is zero. + fn unreserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> Self::Balance { + if value.is_zero() { + return value; + } + + let (remaining, _) = Self::mutate_account(currency_id, who, |account| { + let actual = account.reserved.min(value); + account.reserved = account.reserved.defensive_saturating_sub(actual); + account.free = account.free.defensive_saturating_add(actual); + + Self::deposit_event(Event::Unreserved { + currency_id, + who: who.clone(), + amount: actual, + }); + value.defensive_saturating_sub(actual) + }); + + remaining + } + + /// 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 repatriate_reserved( + currency_id: Self::CurrencyId, + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: BalanceStatus, + ) -> sp_std::result::Result { + if value.is_zero() { + return Ok(value); + } + + if slashed == beneficiary { + return match status { + BalanceStatus::Free => Ok(Self::unreserve(currency_id, slashed, value)), + BalanceStatus::Reserved => Ok(value.saturating_sub(Self::reserved_balance(currency_id, slashed))), + }; + } + + let from_account = Self::accounts(slashed, currency_id); + let to_account = Self::accounts(beneficiary, currency_id); + let actual = from_account.reserved.min(value); + match status { + BalanceStatus::Free => { + Self::set_free_balance( + currency_id, + beneficiary, + to_account.free.defensive_saturating_add(actual), + ); + } + BalanceStatus::Reserved => { + Self::set_reserved_balance( + currency_id, + beneficiary, + to_account.reserved.defensive_saturating_add(actual), + ); + } + } + Self::set_reserved_balance( + currency_id, + slashed, + from_account.reserved.defensive_saturating_sub(actual), + ); + + Self::deposit_event(Event::::ReserveRepatriated { + currency_id, + from: slashed.clone(), + to: beneficiary.clone(), + amount: actual, + status, + }); + Ok(value.defensive_saturating_sub(actual)) + } +} + +impl NamedMultiReservableCurrency for Pallet { + type ReserveIdentifier = T::ReserveIdentifier; + + fn reserved_balance_named( + id: &Self::ReserveIdentifier, + currency_id: Self::CurrencyId, + who: &T::AccountId, + ) -> Self::Balance { + let reserves = Self::reserves(who, currency_id); + reserves + .binary_search_by_key(id, |data| data.id) + .map(|index| reserves[index].amount) + .unwrap_or_default() + } + + /// Move `value` from the free balance from `who` to a named reserve + /// balance. + /// + /// Is a no-op if value to be reserved is zero. + fn reserve_named( + id: &Self::ReserveIdentifier, + currency_id: Self::CurrencyId, + who: &T::AccountId, + value: Self::Balance, + ) -> DispatchResult { + if value.is_zero() { + return Ok(()); + } + + Reserves::::try_mutate(who, currency_id, |reserves| -> DispatchResult { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + // this add can't overflow but just to be defensive. + reserves[index].amount = reserves[index].amount.defensive_saturating_add(value); + } + Err(index) => { + reserves + .try_insert(index, ReserveData { id: *id, amount: value }) + .map_err(|_| Error::::TooManyReserves)?; + } + }; + >::reserve(currency_id, who, value) + }) + } + + /// Unreserve some funds, returning any amount that was unable to be + /// unreserved. + /// + /// Is a no-op if the value to be unreserved is zero. + fn unreserve_named( + id: &Self::ReserveIdentifier, + currency_id: Self::CurrencyId, + who: &T::AccountId, + value: Self::Balance, + ) -> Self::Balance { + if value.is_zero() { + return Zero::zero(); + } + + Reserves::::mutate_exists(who, currency_id, |maybe_reserves| -> Self::Balance { + if let Some(reserves) = maybe_reserves.as_mut() { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + let to_change = cmp::min(reserves[index].amount, value); + + let remain = >::unreserve(currency_id, who, to_change); + + // remain should always be zero but just to be defensive here. + let actual = to_change.defensive_saturating_sub(remain); + + // `actual <= to_change` and `to_change <= amount`, but just to be defensive + // here. + reserves[index].amount = reserves[index].amount.defensive_saturating_sub(actual); + + if reserves[index].amount.is_zero() { + if reserves.len() == 1 { + // no more named reserves + *maybe_reserves = None; + } else { + // remove this named reserve + reserves.remove(index); + } + } + value.defensive_saturating_sub(actual) + } + Err(_) => value, + } + } else { + value + } + }) + } + + /// Slash from reserved balance, returning the amount that was unable to be + /// slashed. + /// + /// Is a no-op if the value to be slashed is zero. + fn slash_reserved_named( + id: &Self::ReserveIdentifier, + currency_id: Self::CurrencyId, + who: &T::AccountId, + value: Self::Balance, + ) -> Self::Balance { + if value.is_zero() { + return Zero::zero(); + } + + Reserves::::mutate(who, currency_id, |reserves| -> Self::Balance { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + let to_change = cmp::min(reserves[index].amount, value); + + let remain = >::slash_reserved(currency_id, who, to_change); + + // remain should always be zero but just to be defensive here. + let actual = to_change.defensive_saturating_sub(remain); + + // `actual <= to_change` and `to_change <= amount` but just to be defensive + // here. + reserves[index].amount = reserves[index].amount.defensive_saturating_sub(actual); + + Self::deposit_event(Event::Slashed { + who: who.clone(), + currency_id, + free_amount: Zero::zero(), + reserved_amount: actual, + }); + value.defensive_saturating_sub(actual) + } + Err(_) => value, + } + }) + } + + /// Move the reserved balance of one account into the balance of another, + /// according to `status`. If `status` is `Reserved`, the balance will be + /// reserved with given `id`. + /// + /// 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 repatriate_reserved_named( + id: &Self::ReserveIdentifier, + currency_id: Self::CurrencyId, + slashed: &T::AccountId, + beneficiary: &T::AccountId, + 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_named(id, currency_id, slashed, value)), + Status::Reserved => Ok(value.saturating_sub(Self::reserved_balance_named(id, currency_id, slashed))), + }; + } + + Reserves::::try_mutate( + slashed, + currency_id, + |reserves| -> Result { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + let to_change = cmp::min(reserves[index].amount, value); + + let actual = if status == Status::Reserved { + // make it the reserved under same identifier + Reserves::::try_mutate( + beneficiary, + currency_id, + |reserves| -> Result { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + let remain = >::repatriate_reserved( + currency_id, + slashed, + beneficiary, + to_change, + status, + )?; + + // remain should always be zero but just to be defensive + // here. + let actual = to_change.defensive_saturating_sub(remain); + + // this add can't overflow but just to be defensive. + reserves[index].amount = + reserves[index].amount.defensive_saturating_add(actual); + + Ok(actual) + } + Err(index) => { + let remain = >::repatriate_reserved( + currency_id, + slashed, + beneficiary, + to_change, + status, + )?; + + // remain should always be zero but just to be defensive + // here + let actual = to_change.defensive_saturating_sub(remain); + + reserves + .try_insert( + index, + ReserveData { + id: *id, + amount: actual, + }, + ) + .map_err(|_| Error::::TooManyReserves)?; + + Ok(actual) + } + } + }, + )? + } else { + let remain = >::repatriate_reserved( + currency_id, + slashed, + beneficiary, + to_change, + status, + )?; + + // remain should always be zero but just to be defensive here + to_change.defensive_saturating_sub(remain) + }; + + // `actual <= to_change` and `to_change <= amount` but just to be defensive + // here. + reserves[index].amount = reserves[index].amount.defensive_saturating_sub(actual); + Ok(value.defensive_saturating_sub(actual)) + } + Err(_) => Ok(value), + } + }, + ) + } +} diff --git a/tokens/src/impl_fungibles.rs b/tokens/src/impl_fungibles.rs new file mode 100644 index 000000000..b0e8d1494 --- /dev/null +++ b/tokens/src/impl_fungibles.rs @@ -0,0 +1,349 @@ +//! Implementation of `fungibles` traits for Tokens Module. +use super::*; + +impl fungibles::Inspect for Pallet { + type AssetId = T::CurrencyId; + type Balance = T::Balance; + + fn total_issuance(asset_id: Self::AssetId) -> Self::Balance { + TotalIssuance::::get(asset_id) + } + + fn minimum_balance(asset_id: Self::AssetId) -> Self::Balance { + Self::ed(asset_id) + } + + fn total_balance(asset_id: Self::AssetId, who: &T::AccountId) -> Self::Balance { + Self::accounts(who, asset_id).total() + } + + fn balance(asset_id: Self::AssetId, who: &T::AccountId) -> Self::Balance { + Self::accounts(who, asset_id).free + } + + /// Get the maximum amount that `who` can withdraw/transfer successfully + /// based on whether the account should be kept alive (`preservation`) or + /// whether we are willing to force the transfer and potentially go below + /// user-level restrictions on the minimum amount of the account. + /// + /// Always less than `free_balance()`. + fn reducible_balance( + asset_id: Self::AssetId, + who: &T::AccountId, + preservation: Preservation, + force: Fortitude, + ) -> Self::Balance { + let a = Self::accounts(who, asset_id); + let mut untouchable = Zero::zero(); + if force == Fortitude::Polite { + // Frozen balance applies to total. Anything on hold therefore gets discounted + // from the limit given by the freezes. + untouchable = a.frozen.saturating_sub(a.reserved); + } + // If we want to keep our provider ref.. + if preservation == Preservation::Preserve + // ..or we don't want the account to die and our provider ref is needed for it to live.. + || preservation == Preservation::Protect && !a.free.is_zero() && + frame_system::Pallet::::providers(who) == 1 + // ..or we don't care about the account dying but our provider ref is required.. + || preservation == Preservation::Expendable && !a.free.is_zero() && + !frame_system::Pallet::::can_dec_provider(who) + { + // ..then the ED needed except for the account in dust removal whitelist. + if !Self::in_dust_removal_whitelist(who) { + untouchable = untouchable.max(Self::ed(asset_id)); + } + } + // Liquid balance is what is neither on hold nor frozen/required for provider. + a.free.saturating_sub(untouchable) + } + + fn can_deposit( + asset_id: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + provenance: Provenance, + ) -> DepositConsequence { + if amount.is_zero() { + return DepositConsequence::Success; + } + + if provenance == Provenance::Minted && TotalIssuance::::get(asset_id).checked_add(&amount).is_none() { + return DepositConsequence::Overflow; + } + + let account = Self::accounts(who, asset_id); + let new_free_balance = match account.free.checked_add(&amount) { + Some(x) if x < Self::ed(asset_id) => return DepositConsequence::BelowMinimum, + Some(x) => x, + None => return DepositConsequence::Overflow, + }; + + match account.reserved.checked_add(&new_free_balance) { + Some(_) => {} + None => return DepositConsequence::Overflow, + }; + + // NOTE: We assume that we are a provider, so don't need to do any checks in the + // case of account creation. + + DepositConsequence::Success + } + + fn can_withdraw( + asset_id: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + ) -> WithdrawConsequence { + if amount.is_zero() { + return WithdrawConsequence::Success; + } + + if TotalIssuance::::get(asset_id).checked_sub(&amount).is_none() { + return WithdrawConsequence::Underflow; + } + + let account = Self::accounts(who, asset_id); + let new_free_balance = match account.free.checked_sub(&amount) { + Some(x) => x, + None => return WithdrawConsequence::BalanceLow, + }; + + let liquid = Self::reducible_balance(asset_id, who, Preservation::Expendable, Fortitude::Polite); + if amount > liquid { + return WithdrawConsequence::Frozen; + } + + // Provider restriction - total account balance cannot be reduced to zero if it + // cannot sustain the loss of a provider reference. + // NOTE: This assumes that the pallet is a provider (which is true). Is this + // ever changes, then this will need to adapt accordingly. + let ed = Self::ed(asset_id); + let success = if new_free_balance < ed && !Self::in_dust_removal_whitelist(who) { + if frame_system::Pallet::::can_dec_provider(who) { + WithdrawConsequence::ReducedToZero(new_free_balance) + } else { + return WithdrawConsequence::WouldDie; + } + } else { + WithdrawConsequence::Success + }; + + let new_total_balance = new_free_balance.saturating_add(account.reserved); + + // Eventual total funds must be no less than the frozen balance. + if new_total_balance < account.frozen { + return WithdrawConsequence::Frozen; + } + + success + } + + fn asset_exists(asset: Self::AssetId) -> bool { + TotalIssuance::::contains_key(asset) + } +} + +impl fungibles::Unbalanced for Pallet { + fn handle_dust(dust: fungibles::Dust) { + T::DustRemoval::on_unbalanced(dust.into_credit()); + } + + /// Forcefully set the balance of `who` to `amount`. + /// + /// If this call executes successfully, you can `assert_eq!(Self::balance(), + /// amount);`. + /// + /// For implementations which include one or more balances on hold, then + /// these are *not* included in the `amount`. + /// + /// This function does its best to force the balance change through, but + /// will not break system invariants such as any Existential Deposits needed + /// or overflows/underflows. If this cannot be done for some reason (e.g. + /// because the account cannot be created, deleted or would overflow) then + /// an `Err` is returned. + fn write_balance( + asset_id: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + ) -> Result, DispatchError> { + let max_reduction = >::reducible_balance( + asset_id, + who, + Preservation::Expendable, + Fortitude::Force, + ); + let (result, maybe_dust) = Self::mutate_account(asset_id, who, |account| -> DispatchResult { + // Make sure the reduction (if there is one) is no more than the maximum + // allowed. + let reduction = account.free.saturating_sub(amount); + ensure!(reduction <= max_reduction, Error::::BalanceTooLow); + + account.free = amount; + Ok(()) + }); + result?; + Ok(maybe_dust) + } + + fn set_total_issuance(asset_id: Self::AssetId, amount: Self::Balance) { + // Balance is the same type and will not overflow + TotalIssuance::::mutate(asset_id, |t| *t = amount); + } +} + +impl fungibles::Balanced for Pallet { + type OnDropCredit = fungibles::DecreaseIssuance; + type OnDropDebt = fungibles::IncreaseIssuance; + + fn done_deposit(asset: Self::AssetId, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Deposited { + currency_id: asset, + who: who.clone(), + amount: amount, + }); + } + + fn done_withdraw(asset: Self::AssetId, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Withdrawn { + currency_id: asset, + who: who.clone(), + amount: amount, + }); + } + + fn done_issue(_asset: Self::AssetId, _amount: Self::Balance) {} + + fn done_rescind(_asset: Self::AssetId, _amount: Self::Balance) {} +} + +impl fungibles::Mutate for Pallet { + fn done_mint_into(asset: Self::AssetId, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Deposited { + currency_id: asset, + who: who.clone(), + amount: amount, + }); + } + + fn done_burn_from(asset: Self::AssetId, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Withdrawn { + currency_id: asset, + who: who.clone(), + amount: amount, + }); + } + + fn done_shelve(asset: Self::AssetId, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Withdrawn { + currency_id: asset, + who: who.clone(), + amount: amount, + }); + } + + fn done_restore(asset: Self::AssetId, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Deposited { + currency_id: asset, + who: who.clone(), + amount: amount, + }); + } + + fn done_transfer(asset: Self::AssetId, source: &T::AccountId, dest: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Transfer { + currency_id: asset, + from: source.clone(), + to: dest.clone(), + amount: amount, + }); + } +} + +impl fungibles::InspectHold for Pallet { + type Reason = (); + + fn total_balance_on_hold(asset: Self::AssetId, who: &T::AccountId) -> Self::Balance { + Self::accounts(who, asset).reserved + } + + /// Get the maximum amount that the `total_balance_on_hold` of `who` can be + /// reduced successfully based on whether we are willing to force the + /// reduction and potentially go below user-level restrictions on the + /// minimum amount of the account. Note: This cannot bring the account into + /// an inconsistent state with regards any required existential deposit. + /// + /// Always less than `total_balance_on_hold()`. + fn reducible_total_balance_on_hold(asset: Self::AssetId, who: &T::AccountId, force: Fortitude) -> Self::Balance { + // The total balance must never drop below the freeze requirements if we're not + // forcing: + let a = Self::accounts(who, asset); + let unavailable = if force == Fortitude::Force { + Self::Balance::zero() + } else { + // The freeze lock applies to the total balance, so we can discount the free + // balance from the amount which the total reserved balance must provide to + // satisfy it. + a.frozen.saturating_sub(a.free) + }; + a.reserved.saturating_sub(unavailable) + } + + fn balance_on_hold(asset_id: Self::AssetId, _reason: &Self::Reason, who: &T::AccountId) -> T::Balance { + Self::accounts(who, asset_id).reserved + } + + fn hold_available(_asset: Self::AssetId, _reason: &Self::Reason, _who: &T::AccountId) -> bool { + true + } +} + +impl fungibles::UnbalancedHold for Pallet { + /// Forcefully set the balance on hold of `who` to `amount`. This is + /// independent of any other balances on hold or the main ("free") balance. + /// + /// If this call executes successfully, you can + /// `assert_eq!(Self::balance_on_hold(), amount);`. + /// + /// This function does its best to force the balance change through, but + /// will not break system invariants such as any Existential Deposits needed + /// or overflows/underflows. If this cannot be done for some reason (e.g. + /// because the account doesn't exist) then an `Err` is returned. + // Implmentation note: This should increment the consumer refs if it moves total + // on hold from zero to non-zero and decrement in the opposite direction. + // + // Since this was not done in the previous logic, this will need either a + // migration or a state item which tracks whether the account is on the old + // logic or new. + fn set_balance_on_hold( + asset: Self::AssetId, + _reason: &Self::Reason, + who: &T::AccountId, + amount: Self::Balance, + ) -> DispatchResult { + // Balance is the same type and will not overflow + let (result, maybe_dust) = Self::try_mutate_account(asset, who, |account, _| -> Result<(), DispatchError> { + let old_reserved = account.reserved; + let delta = old_reserved.max(amount) - old_reserved.min(amount); + + account.reserved = if amount > old_reserved { + account.reserved.checked_add(&delta).ok_or(ArithmeticError::Overflow)? + } else { + account.reserved.checked_sub(&delta).ok_or(ArithmeticError::Underflow)? + }; + + Ok(()) + })?; + + debug_assert!( + maybe_dust.is_none(), + "Does not alter main balance; dust only happens when it is altered; qed" + ); + + Ok(result) + } +} + +impl fungibles::MutateHold for Pallet {} + +// TODO: impl fungibles::InspectFreeze and fungibles::MutateFreeze diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index fe2d50857..b49bb6aac 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -38,8 +38,6 @@ #![allow(clippy::unused_unit)] #![allow(clippy::comparison_chain)] -pub use crate::imbalances::{NegativeImbalance, PositiveImbalance}; - use codec::MaxEncodedLen; use frame_support::{ ensure, log, @@ -51,8 +49,8 @@ use frame_support::{ }, BalanceStatus as Status, Contains, Currency as PalletCurrency, DefensiveSaturating, ExistenceRequirement, Get, Imbalance, LockableCurrency as PalletLockableCurrency, - NamedReservableCurrency as PalletNamedReservableCurrency, ReservableCurrency as PalletReservableCurrency, - SignedImbalance, WithdrawReasons, + NamedReservableCurrency as PalletNamedReservableCurrency, OnUnbalanced, + ReservableCurrency as PalletReservableCurrency, SignedImbalance, WithdrawReasons, }, transactional, BoundedVec, }; @@ -63,7 +61,7 @@ use sp_runtime::{ AtLeast32BitUnsigned, Bounded, CheckedAdd, CheckedSub, MaybeSerializeDeserialize, Member, Saturating, StaticLookup, Zero, }, - ArithmeticError, DispatchError, DispatchResult, FixedPointOperand, RuntimeDebug, TokenError, + ArithmeticError, DispatchError, DispatchResult, FixedPointOperand, RuntimeDebug, }; use sp_std::{cmp, convert::Infallible, marker, prelude::*, vec::Vec}; @@ -74,7 +72,9 @@ use orml_traits::{ MultiReservableCurrency, NamedMultiReservableCurrency, }; -mod imbalances; +mod currency_adapter; +mod impl_currency; +mod impl_fungibles; mod impls; mod mock; mod tests; @@ -85,6 +85,8 @@ mod tests_multicurrency; mod weights; +pub use currency_adapter::CurrencyAdapter; +pub use impl_currency::{NegativeImbalance, PositiveImbalance}; pub use impls::*; pub use weights::WeightInfo; @@ -160,14 +162,13 @@ pub struct AccountData { } impl AccountData { - /// The amount that this account's free balance may not be reduced - /// beyond. - pub(crate) fn frozen(&self) -> Balance { - self.frozen + pub(crate) fn usable(&self) -> Balance { + self.free.saturating_sub(self.frozen) } + /// The total balance in this account including any that is reserved and /// ignoring any frozen. - fn total(&self) -> Balance { + pub(crate) fn total(&self) -> Balance { self.free.saturating_add(self.reserved) } } @@ -180,6 +181,25 @@ pub mod module { use super::*; + pub type CreditOf = fungibles::Credit<::AccountId, Pallet>; + + pub struct DustReceiver(sp_std::marker::PhantomData<(T, GetAccountId)>); + impl OnUnbalanced> for DustReceiver + where + T: Config, + GetAccountId: Get>, + { + fn on_nonzero_unbalanced(amount: CreditOf) { + match GetAccountId::get() { + None => drop(amount), + Some(receiver) => { + let result = as fungibles::Balanced<_>>::resolve(&receiver, amount); + debug_assert!(result.is_ok()); + } + } + } + } + #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; @@ -236,6 +256,9 @@ pub mod module { // The whitelist of accounts that will not be reaped even if its total // is zero or below ED. type DustRemovalWhitelist: Contains; + + /// Handler for the unbalanced reduction when removing a dust account. + type DustRemoval: OnUnbalanced>; } #[pallet::error] @@ -443,7 +466,7 @@ pub mod module { *initial_balance >= T::ExistentialDeposits::get(currency_id), "the balance of any account should always be more than existential deposit.", ); - Pallet::::mutate_account(account_id, *currency_id, |account_data, _| { + Pallet::::mutate_account(account_id, *currency_id, |account_data| { account_data.free = *initial_balance }); TotalIssuance::::mutate(*currency_id, |total_issuance| { @@ -602,7 +625,7 @@ pub mod module { ensure_root(origin)?; let who = T::Lookup::lookup(who)?; - Self::try_mutate_account(&who, currency_id, |account, _| -> DispatchResult { + Self::try_mutate_account(currency_id, &who, |account, _| -> DispatchResult { let mut new_total = new_free.checked_add(&new_reserved).ok_or(ArithmeticError::Overflow)?; let (new_free, new_reserved) = if new_total < T::ExistentialDeposits::get(¤cy_id) { new_total = Zero::zero(); @@ -646,81 +669,12 @@ pub mod module { } impl Pallet { - pub(crate) fn deposit_consequence( - _who: &T::AccountId, - currency_id: T::CurrencyId, - amount: T::Balance, - account: &AccountData, - ) -> DepositConsequence { - if amount.is_zero() { - return DepositConsequence::Success; - } - - if TotalIssuance::::get(currency_id).checked_add(&amount).is_none() { - return DepositConsequence::Overflow; - } - - let new_total_balance = match account.total().checked_add(&amount) { - Some(x) => x, - None => return DepositConsequence::Overflow, - }; - - if new_total_balance < T::ExistentialDeposits::get(¤cy_id) { - return DepositConsequence::BelowMinimum; - } - - // NOTE: We assume that we are a provider, so don't need to do any checks in the - // case of account creation. - - DepositConsequence::Success + fn ed(currency_id: T::CurrencyId) -> T::Balance { + T::ExistentialDeposits::get(¤cy_id) } - pub(crate) fn withdraw_consequence( - who: &T::AccountId, - currency_id: T::CurrencyId, - amount: T::Balance, - account: &AccountData, - ) -> WithdrawConsequence { - if amount.is_zero() { - return WithdrawConsequence::Success; - } - - if TotalIssuance::::get(currency_id).checked_sub(&amount).is_none() { - return WithdrawConsequence::Underflow; - } - - let new_total_balance = match account.total().checked_sub(&amount) { - Some(x) => x, - None => return WithdrawConsequence::BalanceLow, - }; - - // Provider restriction - total account balance cannot be reduced to zero if it - // cannot sustain the loss of a provider reference. - // NOTE: This assumes that the pallet is a provider (which is true). Is this - // ever changes, then this will need to adapt accordingly. - let ed = T::ExistentialDeposits::get(¤cy_id); - let success = if new_total_balance < ed { - if frame_system::Pallet::::can_dec_provider(who) { - WithdrawConsequence::ReducedToZero(new_total_balance) - } else { - return WithdrawConsequence::WouldDie; - } - } else { - WithdrawConsequence::Success - }; - - // Enough free funds to have them be reduced. - let new_free_balance = match account.free.checked_sub(&amount) { - Some(b) => b, - None => return WithdrawConsequence::BalanceLow, - }; - - // Eventual free funds must be no less than the frozen balance. - if new_free_balance < account.frozen() { - return WithdrawConsequence::Frozen; - } - - success + fn in_dust_removal_whitelist(who: &T::AccountId) -> bool { + T::DustRemovalWhitelist::contains(who) } // Ensure that an account can withdraw from their free balance given any @@ -739,55 +693,86 @@ impl Pallet { .checked_sub(&amount) .ok_or(Error::::BalanceTooLow)?; ensure!( - new_balance >= Self::accounts(who, currency_id).frozen(), + new_balance >= Self::accounts(who, currency_id).frozen, Error::::LiquidityRestrictions ); Ok(()) } + /// Mutate an account to some new value, or delete it entirely with `None`. + /// Will enforce `ExistentialDeposit` law, annulling the account as needed. + /// This will do nothing if the result of `f` is an `Err`. + /// + /// It returns both the result from the closure, and an optional amount of + /// dust which should be handled once it is known that all nested mutates + /// that could affect storage items what the dust handler touches have + /// completed. + /// + /// NOTE: Doesn't do any preparatory work for creating a new account, so + /// should only be used when it is known that the account already exists. + /// + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is + /// expected that the caller will do this. pub(crate) fn try_mutate_account( - who: &T::AccountId, currency_id: T::CurrencyId, - f: impl FnOnce(&mut AccountData, bool) -> sp_std::result::Result, - ) -> sp_std::result::Result<(R, Option), E> { + who: &T::AccountId, + f: impl FnOnce(&mut AccountData, bool) -> Result, + ) -> Result<(R, Option), E> { Accounts::::try_mutate_exists(who, currency_id, |maybe_account| { - let existed = maybe_account.is_some(); + let is_new = maybe_account.is_none(); let mut account = maybe_account.take().unwrap_or_default(); - f(&mut account, existed).map(move |result| { - let maybe_endowed = if !existed { Some(account.free) } else { None }; - let mut maybe_dust: Option = None; - let total = account.total(); - *maybe_account = if total < T::ExistentialDeposits::get(¤cy_id) { - // if ED is not zero, but account total is zero, account will be reaped - if total.is_zero() { - None - } else { - if !T::DustRemovalWhitelist::contains(who) { - maybe_dust = Some(total); - } - Some(account) - } + + let result = f(&mut account, is_new)?; + + let maybe_endowed = if is_new { Some(account.free) } else { None }; + + // Handle any steps needed after mutating an account. + // + // This includes DustRemoval unbalancing, in the case than the `new` account's total + // balance is non-zero but below ED. + // + // Updates `maybe_account` to `Some` iff the account has sufficient balance. + // Evaluates `maybe_dust`, which is `Some` containing the dust to be dropped, iff + // some dust should be dropped. + // + // We should never be dropping if reserved is non-zero. Reserved being non-zero + // should imply that we have a consumer ref, so this is economically safe. + let ed = Self::ed(currency_id); + let maybe_dust = if account.free < ed && account.reserved.is_zero() { + if account.free.is_zero() { + None + } else if Self::in_dust_removal_whitelist(who) { + // NOTE: if the account is in the dust removal whitelist, don't drop! + *maybe_account = Some(account); + + None } else { - // Note: if ED is zero, account will never be reaped - Some(account) - }; + Some(account.free) + } + } else { + assert!( + account.free.is_zero() || account.free >= ed || !account.reserved.is_zero() + ); + *maybe_account = Some(account); + None + }; - (maybe_endowed, existed, maybe_account.is_some(), maybe_dust, result) - }) - }) - .map(|(maybe_endowed, existed, exists, maybe_dust, result)| { - if existed && !exists { + let exists = maybe_account.is_some(); + + if !is_new && !exists { // If existed before, decrease account provider. // Ignore the result, because if it failed then there are remaining consumers, // and the account storage in frame_system shouldn't be reaped. let _ = frame_system::Pallet::::dec_providers(who); >::OnKilledTokenAccount::happened(&(who.clone(), currency_id)); - } else if !existed && exists { + } else if is_new && exists { // if new, increase account provider frame_system::Pallet::::inc_providers(who); >::OnNewTokenAccount::happened(&(who.clone(), currency_id)); } + Ok((maybe_endowed, maybe_dust, result)) + }).map(|(maybe_endowed, maybe_dust, result)| { if let Some(endowed) = maybe_endowed { Self::deposit_event(Event::Endowed { currency_id, @@ -797,10 +782,6 @@ impl Pallet { } if let Some(dust_amount) = maybe_dust { - // `OnDust` maybe get/set storage `Accounts` of `who`, trigger handler here - // to avoid some unexpected errors. - >::OnDust::on_dust(who, currency_id, dust_amount); - Self::deposit_event(Event::DustLost { currency_id, who: who.clone(), @@ -812,17 +793,76 @@ impl Pallet { }) } + /// Mutate an account to some new value, or delete it entirely with `None`. + /// Will enforce `ExistentialDeposit` law, annulling the account as needed. + /// + /// It returns both the result from the closure, and an optional amount of + /// dust which should be handled once it is known that all nested mutates + /// that could affect storage items what the dust handler touches have + /// completed. + /// + /// NOTE: Doesn't do any preparatory work for creating a new account, so + /// should only be used when it is known that the account already exists. + /// + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is + /// expected that the caller will do this. pub(crate) fn mutate_account( - who: &T::AccountId, currency_id: T::CurrencyId, - f: impl FnOnce(&mut AccountData, bool) -> R, + who: &T::AccountId, + f: impl FnOnce(&mut AccountData) -> R, ) -> (R, Option) { - Self::try_mutate_account(who, currency_id, |account, existed| -> Result { - Ok(f(account, existed)) + Self::try_mutate_account(currency_id, who, |account, _| -> Result { + Ok(f(account)) }) .expect("Error is infallible; qed") } + /// Mutate an account to some new value, or delete it entirely with `None`. + /// Will enforce `ExistentialDeposit` law, annulling the account as needed. + /// + /// It returns the result from the closure. Any dust is handled through the + /// low-level `fungible::Unbalanced` trap-door for legacy dust management. + /// + /// NOTE: Doesn't do any preparatory work for creating a new account, so + /// should only be used when it is known that the account already exists. + /// + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is + /// expected that the caller will do this. + pub(crate) fn try_mutate_account_handling_dust( + currency_id: T::CurrencyId, + who: &T::AccountId, + f: impl FnOnce(&mut AccountData, bool) -> Result, + ) -> Result { + let (r, maybe_dust) = Self::try_mutate_account(currency_id, who, f)?; + if let Some(dust) = maybe_dust { + >::handle_raw_dust(currency_id, dust); + } + Ok(r) + } + + /// Mutate an account to some new value, or delete it entirely with `None`. + /// Will enforce `ExistentialDeposit` law, annulling the account as needed. + /// + /// It returns the result from the closure. Any dust is handled through the + /// low-level `fungible::Unbalanced` trap-door for legacy dust management. + /// + /// NOTE: Doesn't do any preparatory work for creating a new account, so + /// should only be used when it is known that the account already exists. + /// + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is + /// expected that the caller will do this. + pub(crate) fn mutate_account_handling_dust( + currency_id: T::CurrencyId, + who: &T::AccountId, + f: impl FnOnce(&mut AccountData) -> R, + ) -> R { + let (r, maybe_dust) = Self::mutate_account(currency_id, who, f); + if let Some(dust) = maybe_dust { + >::handle_raw_dust(currency_id, dust); + } + r + } + /// Set free balance of `who` to a new value. /// /// Note: this will not maintain total issuance, and the caller is expected @@ -830,7 +870,7 @@ impl Pallet { /// it, because maybe the account that should be reaped to remain due to /// failed transfer/withdraw dust. pub(crate) fn set_free_balance(currency_id: T::CurrencyId, who: &T::AccountId, amount: T::Balance) { - Self::mutate_account(who, currency_id, |account, _| { + Self::mutate_account(currency_id, who, |account| { account.free = amount; Self::deposit_event(Event::BalanceSet { @@ -849,7 +889,7 @@ impl Pallet { /// it, because maybe the account that should be reaped to remain due to /// failed transfer/withdraw dust. pub(crate) fn set_reserved_balance(currency_id: T::CurrencyId, who: &T::AccountId, amount: T::Balance) { - Self::mutate_account(who, currency_id, |account, _| { + Self::mutate_account(currency_id, who, |account| { account.reserved = amount; Self::deposit_event(Event::BalanceSet { @@ -873,7 +913,7 @@ impl Pallet { let mut total_frozen_after = Zero::zero(); // update account data - Self::mutate_account(who, currency_id, |account, _| { + Self::mutate_account(currency_id, who, |account| { total_frozen_prev = account.frozen; account.frozen = Zero::zero(); for lock in locks.iter() { @@ -950,8 +990,8 @@ impl Pallet { to, amount, )?; - Self::try_mutate_account(to, currency_id, |to_account, _existed| -> DispatchResult { - Self::try_mutate_account(from, currency_id, |from_account, _existed| -> DispatchResult { + Self::try_mutate_account(currency_id, to, |to_account, _| -> DispatchResult { + Self::try_mutate_account(currency_id, from, |from_account, _| -> DispatchResult { from_account.free = from_account .free .checked_sub(&amount) @@ -1023,7 +1063,7 @@ impl Pallet { return Ok(()); } - Self::try_mutate_account(who, currency_id, |account, _existed| -> DispatchResult { + Self::try_mutate_account(currency_id, who, |account, _| -> DispatchResult { Self::ensure_can_withdraw(currency_id, who, amount)?; let previous_total = account.total(); account.free = account.free.defensive_saturating_sub(amount); @@ -1088,15 +1128,15 @@ impl Pallet { who, amount, )?; - Self::try_mutate_account(who, currency_id, |account, existed| -> DispatchResult { + Self::try_mutate_account(currency_id, who, |account, is_new| -> DispatchResult { if require_existed { - ensure!(existed, Error::::DeadAccount); + ensure!(!is_new, Error::::DeadAccount); } else { let ed = T::ExistentialDeposits::get(¤cy_id); // Note: if who is in dust removal whitelist, allow to deposit the amount that // below ED to it. ensure!( - amount >= ed || existed || T::DustRemovalWhitelist::contains(who), + amount >= ed || !is_new || T::DustRemovalWhitelist::contains(who), Error::::ExistentialDeposit ); } @@ -1123,1414 +1163,3 @@ impl Pallet { Ok(amount) } } - -impl MultiCurrency for Pallet { - type CurrencyId = T::CurrencyId; - type Balance = T::Balance; - - fn minimum_balance(currency_id: Self::CurrencyId) -> Self::Balance { - T::ExistentialDeposits::get(¤cy_id) - } - - fn total_issuance(currency_id: Self::CurrencyId) -> Self::Balance { - Self::total_issuance(currency_id) - } - - fn total_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { - Self::accounts(who, currency_id).total() - } - - fn free_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { - Self::accounts(who, currency_id).free - } - - fn ensure_can_withdraw(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - Self::ensure_can_withdraw(currency_id, who, amount) - } - - fn transfer( - currency_id: Self::CurrencyId, - from: &T::AccountId, - to: &T::AccountId, - amount: Self::Balance, - ) -> DispatchResult { - // allow death - Self::do_transfer(currency_id, from, to, amount, ExistenceRequirement::AllowDeath) - } - - fn deposit(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - // do not require existing - Self::do_deposit(currency_id, who, amount, false, true)?; - Ok(()) - } - - fn withdraw(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - // allow death - Self::do_withdraw(currency_id, who, amount, ExistenceRequirement::AllowDeath, true) - } - - // Check if `value` amount of free balance can be slashed from `who`. - fn can_slash(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> bool { - if value.is_zero() { - return true; - } - Self::free_balance(currency_id, who) >= value - } - - /// Is a no-op if `value` to be slashed is zero. - /// - /// NOTE: `slash()` prefers free balance, but assumes that reserve - /// balance can be drawn from in extreme circumstances. `can_slash()` - /// should be used prior to `slash()` to avoid having to draw from - /// reserved funds, however we err on the side of punishment if things - /// are inconsistent or `can_slash` wasn't used appropriately. - fn slash(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> Self::Balance { - if amount.is_zero() { - return amount; - } - - >::OnSlash::on_slash( - currency_id, - who, - amount, - ); - let account = Self::accounts(who, currency_id); - let free_slashed_amount = account.free.min(amount); - // Cannot underflow because free_slashed_amount can never be greater than amount - // but just to be defensive here. - let mut remaining_slash = amount.defensive_saturating_sub(free_slashed_amount); - - // slash free balance - if !free_slashed_amount.is_zero() { - // Cannot underflow becuase free_slashed_amount can never be greater than - // account.free but just to be defensive here. - Self::set_free_balance( - currency_id, - who, - account.free.defensive_saturating_sub(free_slashed_amount), - ); - } - - // slash reserved balance - let reserved_slashed_amount = account.reserved.min(remaining_slash); - - if !reserved_slashed_amount.is_zero() { - // Cannot underflow due to above line but just to be defensive here. - remaining_slash = remaining_slash.defensive_saturating_sub(reserved_slashed_amount); - Self::set_reserved_balance( - currency_id, - who, - account.reserved.defensive_saturating_sub(reserved_slashed_amount), - ); - } - - // Cannot underflow because the slashed value cannot be greater than total - // issuance but just to be defensive here. - TotalIssuance::::mutate(currency_id, |v| { - *v = v.defensive_saturating_sub(amount.defensive_saturating_sub(remaining_slash)) - }); - - Self::deposit_event(Event::Slashed { - currency_id, - who: who.clone(), - free_amount: free_slashed_amount, - reserved_amount: reserved_slashed_amount, - }); - remaining_slash - } -} - -impl MultiCurrencyExtended for Pallet { - type Amount = T::Amount; - - fn update_balance(currency_id: Self::CurrencyId, who: &T::AccountId, by_amount: Self::Amount) -> DispatchResult { - if by_amount.is_zero() { - return Ok(()); - } - - // Ensure this doesn't overflow. There isn't any traits that exposes - // `saturating_abs` so we need to do it manually. - let by_amount_abs = if by_amount == Self::Amount::min_value() { - Self::Amount::max_value() - } else { - by_amount.abs() - }; - - let by_balance = - TryInto::::try_into(by_amount_abs).map_err(|_| Error::::AmountIntoBalanceFailed)?; - if by_amount.is_positive() { - Self::deposit(currency_id, who, by_balance) - } else { - Self::withdraw(currency_id, who, by_balance).map(|_| ()) - } - } -} - -impl MultiLockableCurrency for Pallet { - type Moment = T::BlockNumber; - - // Set a lock on the balance of `who` under `currency_id`. - // Is a no-op if lock amount is zero. - fn set_lock( - lock_id: LockIdentifier, - currency_id: Self::CurrencyId, - who: &T::AccountId, - amount: Self::Balance, - ) -> DispatchResult { - if amount.is_zero() { - return Ok(()); - } - let mut new_lock = Some(BalanceLock { id: lock_id, amount }); - let mut locks = Self::locks(who, currency_id) - .into_iter() - .filter_map(|lock| { - if lock.id == lock_id { - new_lock.take() - } else { - Some(lock) - } - }) - .collect::>(); - if let Some(lock) = new_lock { - locks.push(lock) - } - Self::update_locks(currency_id, who, &locks[..])?; - - Self::deposit_event(Event::LockSet { - lock_id, - currency_id, - who: who.clone(), - amount, - }); - Ok(()) - } - - // Extend a lock on the balance of `who` under `currency_id`. - // Is a no-op if lock amount is zero - fn extend_lock( - lock_id: LockIdentifier, - currency_id: Self::CurrencyId, - who: &T::AccountId, - amount: Self::Balance, - ) -> DispatchResult { - if amount.is_zero() { - return Ok(()); - } - let mut new_lock = Some(BalanceLock { id: lock_id, amount }); - let mut locks = Self::locks(who, currency_id) - .into_iter() - .filter_map(|lock| { - if lock.id == lock_id { - new_lock.take().map(|nl| BalanceLock { - id: lock.id, - amount: lock.amount.max(nl.amount), - }) - } else { - Some(lock) - } - }) - .collect::>(); - if let Some(lock) = new_lock { - locks.push(lock) - } - Self::update_locks(currency_id, who, &locks[..]) - } - - fn remove_lock(lock_id: LockIdentifier, currency_id: Self::CurrencyId, who: &T::AccountId) -> DispatchResult { - let mut locks = Self::locks(who, currency_id); - locks.retain(|lock| lock.id != lock_id); - let locks_vec = locks.to_vec(); - Self::update_locks(currency_id, who, &locks_vec[..])?; - - Self::deposit_event(Event::LockRemoved { - lock_id, - currency_id, - who: who.clone(), - }); - Ok(()) - } -} - -impl MultiReservableCurrency for Pallet { - /// Check if `who` can reserve `value` from their free balance. - /// - /// Always `true` if value to be reserved is zero. - fn can_reserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> bool { - if value.is_zero() { - return true; - } - Self::ensure_can_withdraw(currency_id, who, value).is_ok() - } - - /// Slash from reserved balance, returning any amount that was unable to - /// be slashed. - /// - /// Is a no-op if the value to be slashed is zero. - fn slash_reserved(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> Self::Balance { - if value.is_zero() { - return value; - } - - >::OnSlash::on_slash( - currency_id, - who, - value, - ); - let reserved_balance = Self::reserved_balance(currency_id, who); - let actual = reserved_balance.min(value); - Self::mutate_account(who, currency_id, |account, _| { - // ensured reserved_balance >= actual but just to be defensive here. - account.reserved = reserved_balance.defensive_saturating_sub(actual); - }); - TotalIssuance::::mutate(currency_id, |v| *v = v.defensive_saturating_sub(actual)); - - Self::deposit_event(Event::Slashed { - currency_id, - who: who.clone(), - free_amount: Zero::zero(), - reserved_amount: actual, - }); - value.defensive_saturating_sub(actual) - } - - fn reserved_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { - Self::accounts(who, currency_id).reserved - } - - /// Move `value` from the free balance from `who` to their reserved - /// balance. - /// - /// Is a no-op if value to be reserved is zero. - fn reserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> DispatchResult { - if value.is_zero() { - return Ok(()); - } - Self::ensure_can_withdraw(currency_id, who, value)?; - - Self::mutate_account(who, currency_id, |account, _| { - account.free = account.free.defensive_saturating_sub(value); - account.reserved = account.reserved.defensive_saturating_add(value); - - Self::deposit_event(Event::Reserved { - currency_id, - who: who.clone(), - amount: value, - }); - }); - - Ok(()) - } - - /// Unreserve some funds, returning any amount that was unable to be - /// unreserved. - /// - /// Is a no-op if the value to be unreserved is zero. - fn unreserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> Self::Balance { - if value.is_zero() { - return value; - } - - let (remaining, _) = Self::mutate_account(who, currency_id, |account, _| { - let actual = account.reserved.min(value); - account.reserved = account.reserved.defensive_saturating_sub(actual); - account.free = account.free.defensive_saturating_add(actual); - - Self::deposit_event(Event::Unreserved { - currency_id, - who: who.clone(), - amount: actual, - }); - value.defensive_saturating_sub(actual) - }); - - remaining - } - - /// 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 repatriate_reserved( - currency_id: Self::CurrencyId, - slashed: &T::AccountId, - beneficiary: &T::AccountId, - value: Self::Balance, - status: BalanceStatus, - ) -> sp_std::result::Result { - if value.is_zero() { - return Ok(value); - } - - if slashed == beneficiary { - return match status { - BalanceStatus::Free => Ok(Self::unreserve(currency_id, slashed, value)), - BalanceStatus::Reserved => Ok(value.saturating_sub(Self::reserved_balance(currency_id, slashed))), - }; - } - - let from_account = Self::accounts(slashed, currency_id); - let to_account = Self::accounts(beneficiary, currency_id); - let actual = from_account.reserved.min(value); - match status { - BalanceStatus::Free => { - Self::set_free_balance( - currency_id, - beneficiary, - to_account.free.defensive_saturating_add(actual), - ); - } - BalanceStatus::Reserved => { - Self::set_reserved_balance( - currency_id, - beneficiary, - to_account.reserved.defensive_saturating_add(actual), - ); - } - } - Self::set_reserved_balance( - currency_id, - slashed, - from_account.reserved.defensive_saturating_sub(actual), - ); - - Self::deposit_event(Event::::ReserveRepatriated { - currency_id, - from: slashed.clone(), - to: beneficiary.clone(), - amount: actual, - status, - }); - Ok(value.defensive_saturating_sub(actual)) - } -} - -impl NamedMultiReservableCurrency for Pallet { - type ReserveIdentifier = T::ReserveIdentifier; - - fn reserved_balance_named( - id: &Self::ReserveIdentifier, - currency_id: Self::CurrencyId, - who: &T::AccountId, - ) -> Self::Balance { - let reserves = Self::reserves(who, currency_id); - reserves - .binary_search_by_key(id, |data| data.id) - .map(|index| reserves[index].amount) - .unwrap_or_default() - } - - /// Move `value` from the free balance from `who` to a named reserve - /// balance. - /// - /// Is a no-op if value to be reserved is zero. - fn reserve_named( - id: &Self::ReserveIdentifier, - currency_id: Self::CurrencyId, - who: &T::AccountId, - value: Self::Balance, - ) -> DispatchResult { - if value.is_zero() { - return Ok(()); - } - - Reserves::::try_mutate(who, currency_id, |reserves| -> DispatchResult { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - // this add can't overflow but just to be defensive. - reserves[index].amount = reserves[index].amount.defensive_saturating_add(value); - } - Err(index) => { - reserves - .try_insert(index, ReserveData { id: *id, amount: value }) - .map_err(|_| Error::::TooManyReserves)?; - } - }; - >::reserve(currency_id, who, value) - }) - } - - /// Unreserve some funds, returning any amount that was unable to be - /// unreserved. - /// - /// Is a no-op if the value to be unreserved is zero. - fn unreserve_named( - id: &Self::ReserveIdentifier, - currency_id: Self::CurrencyId, - who: &T::AccountId, - value: Self::Balance, - ) -> Self::Balance { - if value.is_zero() { - return Zero::zero(); - } - - Reserves::::mutate_exists(who, currency_id, |maybe_reserves| -> Self::Balance { - if let Some(reserves) = maybe_reserves.as_mut() { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - let to_change = cmp::min(reserves[index].amount, value); - - let remain = >::unreserve(currency_id, who, to_change); - - // remain should always be zero but just to be defensive here. - let actual = to_change.defensive_saturating_sub(remain); - - // `actual <= to_change` and `to_change <= amount`, but just to be defensive - // here. - reserves[index].amount = reserves[index].amount.defensive_saturating_sub(actual); - - if reserves[index].amount.is_zero() { - if reserves.len() == 1 { - // no more named reserves - *maybe_reserves = None; - } else { - // remove this named reserve - reserves.remove(index); - } - } - value.defensive_saturating_sub(actual) - } - Err(_) => value, - } - } else { - value - } - }) - } - - /// Slash from reserved balance, returning the amount that was unable to be - /// slashed. - /// - /// Is a no-op if the value to be slashed is zero. - fn slash_reserved_named( - id: &Self::ReserveIdentifier, - currency_id: Self::CurrencyId, - who: &T::AccountId, - value: Self::Balance, - ) -> Self::Balance { - if value.is_zero() { - return Zero::zero(); - } - - Reserves::::mutate(who, currency_id, |reserves| -> Self::Balance { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - let to_change = cmp::min(reserves[index].amount, value); - - let remain = >::slash_reserved(currency_id, who, to_change); - - // remain should always be zero but just to be defensive here. - let actual = to_change.defensive_saturating_sub(remain); - - // `actual <= to_change` and `to_change <= amount` but just to be defensive - // here. - reserves[index].amount = reserves[index].amount.defensive_saturating_sub(actual); - - Self::deposit_event(Event::Slashed { - who: who.clone(), - currency_id, - free_amount: Zero::zero(), - reserved_amount: actual, - }); - value.defensive_saturating_sub(actual) - } - Err(_) => value, - } - }) - } - - /// Move the reserved balance of one account into the balance of another, - /// according to `status`. If `status` is `Reserved`, the balance will be - /// reserved with given `id`. - /// - /// 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 repatriate_reserved_named( - id: &Self::ReserveIdentifier, - currency_id: Self::CurrencyId, - slashed: &T::AccountId, - beneficiary: &T::AccountId, - 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_named(id, currency_id, slashed, value)), - Status::Reserved => Ok(value.saturating_sub(Self::reserved_balance_named(id, currency_id, slashed))), - }; - } - - Reserves::::try_mutate( - slashed, - currency_id, - |reserves| -> Result { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - let to_change = cmp::min(reserves[index].amount, value); - - let actual = if status == Status::Reserved { - // make it the reserved under same identifier - Reserves::::try_mutate( - beneficiary, - currency_id, - |reserves| -> Result { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - let remain = >::repatriate_reserved( - currency_id, - slashed, - beneficiary, - to_change, - status, - )?; - - // remain should always be zero but just to be defensive - // here. - let actual = to_change.defensive_saturating_sub(remain); - - // this add can't overflow but just to be defensive. - reserves[index].amount = - reserves[index].amount.defensive_saturating_add(actual); - - Ok(actual) - } - Err(index) => { - let remain = >::repatriate_reserved( - currency_id, - slashed, - beneficiary, - to_change, - status, - )?; - - // remain should always be zero but just to be defensive - // here - let actual = to_change.defensive_saturating_sub(remain); - - reserves - .try_insert( - index, - ReserveData { - id: *id, - amount: actual, - }, - ) - .map_err(|_| Error::::TooManyReserves)?; - - Ok(actual) - } - } - }, - )? - } else { - let remain = >::repatriate_reserved( - currency_id, - slashed, - beneficiary, - to_change, - status, - )?; - - // remain should always be zero but just to be defensive here - to_change.defensive_saturating_sub(remain) - }; - - // `actual <= to_change` and `to_change <= amount` but just to be defensive - // here. - reserves[index].amount = reserves[index].amount.defensive_saturating_sub(actual); - Ok(value.defensive_saturating_sub(actual)) - } - Err(_) => Ok(value), - } - }, - ) - } -} - -impl fungibles::Inspect for Pallet { - type AssetId = T::CurrencyId; - type Balance = T::Balance; - - fn total_issuance(asset_id: Self::AssetId) -> Self::Balance { - Self::total_issuance(asset_id) - } - - fn minimum_balance(asset_id: Self::AssetId) -> Self::Balance { - T::ExistentialDeposits::get(&asset_id) - } - - fn balance(asset_id: Self::AssetId, who: &T::AccountId) -> Self::Balance { - Self::accounts(who, asset_id).total() - } - - fn total_balance(asset_id: Self::AssetId, who: &T::AccountId) -> Self::Balance { - Self::accounts(who, asset_id).total() - } - - fn reducible_balance( - asset_id: Self::AssetId, - who: &T::AccountId, - preservation: Preservation, - _force: Fortitude, - ) -> Self::Balance { - let a = Self::accounts(who, asset_id); - // Liquid balance is what is neither reserved nor locked/frozen. - let liquid = a.free.saturating_sub(a.frozen); - if frame_system::Pallet::::can_dec_provider(who) && !matches!(preservation, Preservation::Protect) { - 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::ExistentialDeposits::get(&asset_id).saturating_sub(a.total().saturating_sub(liquid)); - liquid.saturating_sub(must_remain_to_exist) - } - } - - fn can_deposit( - asset_id: Self::AssetId, - who: &T::AccountId, - amount: Self::Balance, - _provenance: Provenance, - ) -> DepositConsequence { - Self::deposit_consequence(who, asset_id, amount, &Self::accounts(who, asset_id)) - } - - fn can_withdraw( - asset_id: Self::AssetId, - who: &T::AccountId, - amount: Self::Balance, - ) -> WithdrawConsequence { - Self::withdraw_consequence(who, asset_id, amount, &Self::accounts(who, asset_id)) - } - - fn asset_exists(asset: Self::AssetId) -> bool { - TotalIssuance::::contains_key(asset) - } -} - -impl fungibles::Mutate for Pallet { - fn mint_into( - asset_id: Self::AssetId, - who: &T::AccountId, - amount: Self::Balance, - ) -> Result { - Self::deposit_consequence(who, asset_id, amount, &Self::accounts(who, asset_id)).into_result()?; - // do not require existing - Self::do_deposit(asset_id, who, amount, false, true) - } - - fn burn_from( - asset_id: Self::AssetId, - who: &T::AccountId, - amount: Self::Balance, - // TODO: Respect precision - _precision: Precision, - // TODO: Respect fortitude - _fortitude: Fortitude, - ) -> Result { - let extra = - Self::withdraw_consequence(who, asset_id, amount, &Self::accounts(who, asset_id)).into_result(false)?; - let actual = amount.defensive_saturating_add(extra); - // allow death - Self::do_withdraw(asset_id, who, actual, ExistenceRequirement::AllowDeath, true).map(|_| actual) - } - - fn transfer( - asset_id: Self::AssetId, - source: &T::AccountId, - dest: &T::AccountId, - amount: T::Balance, - preservation: Preservation, - ) -> Result { - let existence_requirement = match preservation { - Preservation::Expendable => ExistenceRequirement::AllowDeath, - Preservation::Protect | Preservation::Preserve => ExistenceRequirement::KeepAlive, - }; - Self::do_transfer(asset_id, source, dest, amount, existence_requirement).map(|_| amount) - } -} - -impl fungibles::Unbalanced for Pallet { - fn handle_dust(_dust: fungibles::Dust) { - // Dust is handled in account mutate method - } - - fn write_balance( - asset_id: Self::AssetId, - who: &T::AccountId, - amount: Self::Balance, - ) -> Result, DispatchError> { - // Balance is the same type and will not overflow - let (_, dust_amount) = Self::try_mutate_account(who, asset_id, |account, _| -> Result<(), DispatchError> { - // free = new_balance - reserved - account.free = amount.checked_sub(&account.reserved).ok_or(TokenError::BelowMinimum)?; - - Self::deposit_event(Event::BalanceSet { - currency_id: asset_id, - who: who.clone(), - free: account.free, - reserved: account.reserved, - }); - - Ok(()) - })?; - - Ok(dust_amount) - } - - fn set_total_issuance(asset_id: Self::AssetId, amount: Self::Balance) { - // Balance is the same type and will not overflow - TotalIssuance::::mutate(asset_id, |t| *t = amount); - - Self::deposit_event(Event::TotalIssuanceSet { - currency_id: asset_id, - amount, - }); - } - - fn decrease_balance( - asset: Self::AssetId, - who: &T::AccountId, - mut amount: Self::Balance, - precision: Precision, - preservation: Preservation, - force: Fortitude, - ) -> Result { - let old_balance = as fungibles::Inspect>::balance(asset, who); - let free = as fungibles::Inspect>::reducible_balance(asset, who, preservation, force); - if let Precision::BestEffort = precision { - amount = amount.min(free); - } - let new_balance = old_balance.checked_sub(&amount).ok_or(TokenError::FundsUnavailable)?; - // Original implementation was not returning burnt dust in result - let dust_burnt = Self::write_balance(asset, who, new_balance)?.unwrap_or_default(); - Ok(old_balance.saturating_sub(new_balance).saturating_add(dust_burnt)) - } -} - -type ReasonOf =

::AccountId>>::Reason; -impl fungibles::InspectHold for Pallet { - type Reason = (); - - fn balance_on_hold(asset_id: Self::AssetId, _reason: &Self::Reason, who: &T::AccountId) -> T::Balance { - Self::accounts(who, asset_id).reserved - } - - fn total_balance_on_hold(asset: Self::AssetId, who: &T::AccountId) -> Self::Balance { - Self::accounts(who, asset).reserved - } - - fn reducible_total_balance_on_hold(_asset: Self::AssetId, _who: &T::AccountId, _force: Fortitude) -> Self::Balance { - 0u32.into() - } - - fn hold_available(_asset: Self::AssetId, _reason: &Self::Reason, _who: &T::AccountId) -> bool { - true - } - - fn can_hold(asset_id: Self::AssetId, _reason: &Self::Reason, who: &T::AccountId, amount: T::Balance) -> bool { - let a = Self::accounts(who, asset_id); - let min_balance = T::ExistentialDeposits::get(&asset_id).max(a.frozen); - 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 fungibles::MutateHold for Pallet { - fn hold( - asset_id: Self::AssetId, - _reason: &ReasonOf, - who: &T::AccountId, - amount: Self::Balance, - ) -> DispatchResult { - as MultiReservableCurrency<_>>::reserve(asset_id, who, amount) - } - - fn release( - asset_id: Self::AssetId, - _reason: &ReasonOf, - who: &T::AccountId, - amount: Self::Balance, - precision: Precision, - ) -> Result { - if amount.is_zero() { - return Ok(amount); - } - - // Done on a best-effort basis. - let (released, _) = - Self::try_mutate_account(who, asset_id, |a, _existed| -> Result { - let new_free = a.free.saturating_add(amount.min(a.reserved)); - let actual = new_free.defensive_saturating_sub(a.free); - // Guaranteed to be <= amount and <= a.reserved - ensure!( - matches!(precision, Precision::BestEffort) || actual == amount, - Error::::BalanceTooLow - ); - a.free = new_free; - a.reserved = a.reserved.saturating_sub(actual); - - Self::deposit_event(Event::Unreserved { - currency_id: asset_id, - who: who.clone(), - amount, - }); - Ok(actual) - })?; - - Ok(released) - } - - fn transfer_on_hold( - asset_id: Self::AssetId, - reason: &ReasonOf, - source: &T::AccountId, - dest: &T::AccountId, - amount: Self::Balance, - precision: Precision, - restriction: Restriction, - _fortitude: Fortitude, - ) -> Result { - let status = if restriction == Restriction::OnHold { - Status::Reserved - } else { - Status::Free - }; - ensure!( - amount <= >::balance_on_hold(asset_id, reason, source) - || precision == Precision::BestEffort, - Error::::BalanceTooLow - ); - let gap = Self::repatriate_reserved(asset_id, source, dest, amount, status)?; - // return actual transferred amount - Ok(amount.saturating_sub(gap)) - } -} - -impl fungibles::UnbalancedHold for Pallet { - fn set_balance_on_hold( - asset: Self::AssetId, - _reason: &Self::Reason, - who: &T::AccountId, - amount: Self::Balance, - ) -> DispatchResult { - // Balance is the same type and will not overflow - Self::try_mutate_account(who, asset, |account, _| -> Result<(), DispatchError> { - let old_reserved = account.reserved; - account.reserved = amount; - // free = free + old - new - account.free = account - .free - .checked_add(&old_reserved) - .ok_or(ArithmeticError::Overflow)? - .checked_sub(&account.reserved) - .ok_or(TokenError::BelowMinimum)?; - - Self::deposit_event(Event::BalanceSet { - currency_id: asset, - who: who.clone(), - free: account.free, - reserved: account.reserved, - }); - - Ok(()) - }) - .map(|_| ()) - } -} - -pub struct CurrencyAdapter(marker::PhantomData<(T, GetCurrencyId)>); - -impl PalletCurrency for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - type Balance = T::Balance; - type PositiveImbalance = PositiveImbalance; - type NegativeImbalance = NegativeImbalance; - - fn total_balance(who: &T::AccountId) -> Self::Balance { - as MultiCurrency<_>>::total_balance(GetCurrencyId::get(), who) - } - - fn can_slash(who: &T::AccountId, value: Self::Balance) -> bool { - as MultiCurrency<_>>::can_slash(GetCurrencyId::get(), who, value) - } - - fn total_issuance() -> Self::Balance { - as MultiCurrency<_>>::total_issuance(GetCurrencyId::get()) - } - - fn minimum_balance() -> Self::Balance { - as MultiCurrency<_>>::minimum_balance(GetCurrencyId::get()) - } - - fn burn(mut amount: Self::Balance) -> Self::PositiveImbalance { - if amount.is_zero() { - return PositiveImbalance::zero(); - } - let currency_id = GetCurrencyId::get(); - TotalIssuance::::mutate(currency_id, |issued| { - *issued = issued.checked_sub(&amount).unwrap_or_else(|| { - amount = *issued; - Zero::zero() - }) - }); - - Pallet::::deposit_event(Event::TotalIssuanceSet { - currency_id, - amount: Self::total_issuance(), - }); - PositiveImbalance::new(amount) - } - - fn issue(mut amount: Self::Balance) -> Self::NegativeImbalance { - if amount.is_zero() { - return NegativeImbalance::zero(); - } - TotalIssuance::::mutate(GetCurrencyId::get(), |issued| { - *issued = issued.checked_add(&amount).unwrap_or_else(|| { - amount = Self::Balance::max_value().defensive_saturating_sub(*issued); - Self::Balance::max_value() - }) - }); - - Pallet::::deposit_event(Event::TotalIssuanceSet { - currency_id: GetCurrencyId::get(), - amount: Self::total_issuance(), - }); - NegativeImbalance::new(amount) - } - - fn free_balance(who: &T::AccountId) -> Self::Balance { - as MultiCurrency<_>>::free_balance(GetCurrencyId::get(), who) - } - - fn ensure_can_withdraw( - who: &T::AccountId, - amount: Self::Balance, - _reasons: WithdrawReasons, - _new_balance: Self::Balance, - ) -> DispatchResult { - as MultiCurrency<_>>::ensure_can_withdraw(GetCurrencyId::get(), who, amount) - } - - fn transfer( - source: &T::AccountId, - dest: &T::AccountId, - value: Self::Balance, - existence_requirement: ExistenceRequirement, - ) -> DispatchResult { - Pallet::::do_transfer(GetCurrencyId::get(), source, dest, value, existence_requirement) - } - - fn slash(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { - if value.is_zero() { - return (Self::NegativeImbalance::zero(), value); - } - - let currency_id = GetCurrencyId::get(); - let account = Pallet::::accounts(who, currency_id); - let free_slashed_amount = account.free.min(value); - let mut remaining_slash = value.defensive_saturating_sub(free_slashed_amount); - - // slash free balance - if !free_slashed_amount.is_zero() { - Pallet::::set_free_balance( - currency_id, - who, - account.free.defensive_saturating_sub(free_slashed_amount), - ); - } - - // slash reserved balance - if !remaining_slash.is_zero() { - let reserved_slashed_amount = account.reserved.min(remaining_slash); - remaining_slash = remaining_slash.defensive_saturating_sub(reserved_slashed_amount); - Pallet::::set_reserved_balance( - currency_id, - who, - account.reserved.defensive_saturating_sub(reserved_slashed_amount), - ); - - Pallet::::deposit_event(Event::Slashed { - currency_id, - who: who.clone(), - free_amount: free_slashed_amount, - reserved_amount: reserved_slashed_amount, - }); - ( - Self::NegativeImbalance::new(free_slashed_amount.saturating_add(reserved_slashed_amount)), - remaining_slash, - ) - } else { - Pallet::::deposit_event(Event::Slashed { - currency_id, - who: who.clone(), - free_amount: value, - reserved_amount: Zero::zero(), - }); - (Self::NegativeImbalance::new(value), remaining_slash) - } - } - - /// Deposit some `value` into the free balance of an existing target account - /// `who`. - fn deposit_into_existing( - who: &T::AccountId, - value: Self::Balance, - ) -> sp_std::result::Result { - // do not change total issuance - Pallet::::do_deposit(GetCurrencyId::get(), who, value, true, false).map(|_| PositiveImbalance::new(value)) - } - - /// Deposit some `value` into the free balance of `who`, possibly creating a - /// new account. - fn deposit_creating(who: &T::AccountId, value: Self::Balance) -> Self::PositiveImbalance { - // do not change total issuance - Pallet::::do_deposit(GetCurrencyId::get(), who, value, false, false) - .map_or_else(|_| Self::PositiveImbalance::zero(), |_| PositiveImbalance::new(value)) - } - - fn withdraw( - who: &T::AccountId, - value: Self::Balance, - _reasons: WithdrawReasons, - liveness: ExistenceRequirement, - ) -> sp_std::result::Result { - // do not change total issuance - Pallet::::do_withdraw(GetCurrencyId::get(), who, value, liveness, false) - .map(|_| Self::NegativeImbalance::new(value)) - } - - fn make_free_balance_be( - who: &T::AccountId, - value: Self::Balance, - ) -> SignedImbalance { - let currency_id = GetCurrencyId::get(); - Pallet::::try_mutate_account( - who, - currency_id, - |account, existed| -> Result, ()> { - // If we're attempting to set an existing account to less than ED, then - // bypass the entire operation. It's a no-op if you follow it through, but - // since this is an instance where we might account for a negative imbalance - // (in the dust cleaner of set_account) before we account for its actual - // equal and opposite cause (returned as an Imbalance), then in the - // instance that there's no other accounts on the system at all, we might - // underflow the issuance and our arithmetic will be off. - let ed = T::ExistentialDeposits::get(¤cy_id); - ensure!(value.saturating_add(account.reserved) >= ed || existed, ()); - - let imbalance = if account.free <= value { - SignedImbalance::Positive(PositiveImbalance::new(value.saturating_sub(account.free))) - } else { - SignedImbalance::Negative(NegativeImbalance::new(account.free.saturating_sub(value))) - }; - account.free = value; - - Pallet::::deposit_event(Event::BalanceSet { - currency_id, - who: who.clone(), - free: value, - reserved: account.reserved, - }); - Ok(imbalance) - }, - ) - .map(|(imbalance, _)| imbalance) - .unwrap_or_else(|_| SignedImbalance::Positive(Self::PositiveImbalance::zero())) - } -} - -impl PalletReservableCurrency for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - fn can_reserve(who: &T::AccountId, value: Self::Balance) -> bool { - as MultiReservableCurrency<_>>::can_reserve(GetCurrencyId::get(), who, value) - } - - fn slash_reserved(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { - let actual = as MultiReservableCurrency<_>>::slash_reserved(GetCurrencyId::get(), who, value); - (Self::NegativeImbalance::zero(), actual) - } - - fn reserved_balance(who: &T::AccountId) -> Self::Balance { - as MultiReservableCurrency<_>>::reserved_balance(GetCurrencyId::get(), who) - } - - fn reserve(who: &T::AccountId, value: Self::Balance) -> DispatchResult { - as MultiReservableCurrency<_>>::reserve(GetCurrencyId::get(), who, value) - } - - fn unreserve(who: &T::AccountId, value: Self::Balance) -> Self::Balance { - as MultiReservableCurrency<_>>::unreserve(GetCurrencyId::get(), who, value) - } - - fn repatriate_reserved( - slashed: &T::AccountId, - beneficiary: &T::AccountId, - value: Self::Balance, - status: Status, - ) -> sp_std::result::Result { - as MultiReservableCurrency<_>>::repatriate_reserved( - GetCurrencyId::get(), - slashed, - beneficiary, - value, - status, - ) - } -} - -impl PalletNamedReservableCurrency for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - type ReserveIdentifier = T::ReserveIdentifier; - - fn reserved_balance_named(id: &Self::ReserveIdentifier, who: &T::AccountId) -> Self::Balance { - as NamedMultiReservableCurrency<_>>::reserved_balance_named(id, GetCurrencyId::get(), who) - } - - fn reserve_named(id: &Self::ReserveIdentifier, who: &T::AccountId, value: Self::Balance) -> DispatchResult { - as NamedMultiReservableCurrency<_>>::reserve_named(id, GetCurrencyId::get(), who, value) - } - - fn unreserve_named(id: &Self::ReserveIdentifier, who: &T::AccountId, value: Self::Balance) -> Self::Balance { - as NamedMultiReservableCurrency<_>>::unreserve_named(id, GetCurrencyId::get(), who, value) - } - - fn slash_reserved_named( - id: &Self::ReserveIdentifier, - who: &T::AccountId, - value: Self::Balance, - ) -> (Self::NegativeImbalance, Self::Balance) { - let actual = - as NamedMultiReservableCurrency<_>>::slash_reserved_named(id, GetCurrencyId::get(), who, value); - (Self::NegativeImbalance::zero(), actual) - } - - fn repatriate_reserved_named( - id: &Self::ReserveIdentifier, - slashed: &T::AccountId, - beneficiary: &T::AccountId, - value: Self::Balance, - status: Status, - ) -> sp_std::result::Result { - as NamedMultiReservableCurrency<_>>::repatriate_reserved_named( - id, - GetCurrencyId::get(), - slashed, - beneficiary, - value, - status, - ) - } -} - -impl PalletLockableCurrency for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - type Moment = T::BlockNumber; - type MaxLocks = (); - - fn set_lock(id: LockIdentifier, who: &T::AccountId, amount: Self::Balance, _reasons: WithdrawReasons) { - let _ = as MultiLockableCurrency<_>>::set_lock(id, GetCurrencyId::get(), who, amount); - } - - fn extend_lock(id: LockIdentifier, who: &T::AccountId, amount: Self::Balance, _reasons: WithdrawReasons) { - let _ = as MultiLockableCurrency<_>>::extend_lock(id, GetCurrencyId::get(), who, amount); - } - - fn remove_lock(id: LockIdentifier, who: &T::AccountId) { - let _ = as MultiLockableCurrency<_>>::remove_lock(id, GetCurrencyId::get(), who); - } -} - -impl TransferAll for Pallet { - #[transactional] - fn transfer_all(source: &T::AccountId, dest: &T::AccountId) -> DispatchResult { - Accounts::::iter_prefix(source).try_for_each(|(currency_id, account_data)| -> DispatchResult { - // allow death - Self::do_transfer( - currency_id, - source, - dest, - account_data.free, - ExistenceRequirement::AllowDeath, - ) - }) - } -} - -impl fungible::Inspect for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - type Balance = T::Balance; - - fn total_issuance() -> Self::Balance { - as fungibles::Inspect<_>>::total_issuance(GetCurrencyId::get()) - } - fn minimum_balance() -> Self::Balance { - as fungibles::Inspect<_>>::minimum_balance(GetCurrencyId::get()) - } - fn balance(who: &T::AccountId) -> Self::Balance { - as fungibles::Inspect<_>>::balance(GetCurrencyId::get(), who) - } - fn total_balance(who: &T::AccountId) -> Self::Balance { - as fungibles::Inspect<_>>::total_balance(GetCurrencyId::get(), who) - } - fn reducible_balance(who: &T::AccountId, preservation: Preservation, fortitude: Fortitude) -> Self::Balance { - as fungibles::Inspect<_>>::reducible_balance(GetCurrencyId::get(), who, preservation, fortitude) - } - fn can_deposit(who: &T::AccountId, amount: Self::Balance, provenance: Provenance) -> DepositConsequence { - as fungibles::Inspect<_>>::can_deposit(GetCurrencyId::get(), who, amount, provenance) - } - fn can_withdraw(who: &T::AccountId, amount: Self::Balance) -> WithdrawConsequence { - as fungibles::Inspect<_>>::can_withdraw(GetCurrencyId::get(), who, amount) - } -} - -impl fungible::Mutate for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - fn mint_into(who: &T::AccountId, amount: Self::Balance) -> Result { - as fungibles::Mutate<_>>::mint_into(GetCurrencyId::get(), who, amount) - } - fn burn_from( - who: &T::AccountId, - amount: Self::Balance, - precision: Precision, - fortitude: Fortitude, - ) -> Result { - as fungibles::Mutate<_>>::burn_from(GetCurrencyId::get(), who, amount, precision, fortitude) - } - - fn transfer( - source: &T::AccountId, - dest: &T::AccountId, - amount: T::Balance, - preservation: Preservation, - ) -> Result { - as fungibles::Mutate<_>>::transfer(GetCurrencyId::get(), source, dest, amount, preservation) - } -} - -impl fungible::Unbalanced for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - fn handle_dust(_dust: fungible::Dust) { - // Dust is handled in account mutate method - } - - fn write_balance(who: &T::AccountId, amount: Self::Balance) -> Result, DispatchError> { - as fungibles::Unbalanced<_>>::write_balance(GetCurrencyId::get(), who, amount) - } - fn set_total_issuance(amount: Self::Balance) { - as fungibles::Unbalanced<_>>::set_total_issuance(GetCurrencyId::get(), amount) - } -} - -type ReasonOfFungible =

::AccountId>>::Reason; -impl fungible::InspectHold for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - type Reason = as fungibles::InspectHold>::Reason; - - fn balance_on_hold(reason: &Self::Reason, who: &T::AccountId) -> T::Balance { - as fungibles::InspectHold<_>>::balance_on_hold(GetCurrencyId::get(), reason, who) - } - fn total_balance_on_hold(who: &T::AccountId) -> Self::Balance { - as fungibles::InspectHold<_>>::total_balance_on_hold(GetCurrencyId::get(), who) - } - fn reducible_total_balance_on_hold(who: &T::AccountId, force: Fortitude) -> Self::Balance { - as fungibles::InspectHold<_>>::reducible_total_balance_on_hold(GetCurrencyId::get(), who, force) - } - fn hold_available(reason: &Self::Reason, who: &T::AccountId) -> bool { - as fungibles::InspectHold<_>>::hold_available(GetCurrencyId::get(), reason, who) - } - fn can_hold(reason: &Self::Reason, who: &T::AccountId, amount: T::Balance) -> bool { - as fungibles::InspectHold<_>>::can_hold(GetCurrencyId::get(), reason, who, amount) - } -} - -impl fungible::MutateHold for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - fn hold(reason: &ReasonOfFungible, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - as fungibles::MutateHold<_>>::hold(GetCurrencyId::get(), reason, who, amount) - } - fn release( - reason: &ReasonOfFungible, - who: &T::AccountId, - amount: Self::Balance, - precision: Precision, - ) -> Result { - as fungibles::MutateHold<_>>::release(GetCurrencyId::get(), reason, who, amount, precision) - } - fn transfer_on_hold( - reason: &ReasonOfFungible, - source: &T::AccountId, - dest: &T::AccountId, - amount: Self::Balance, - precision: Precision, - restriction: Restriction, - fortitude: Fortitude, - ) -> Result { - as fungibles::MutateHold<_>>::transfer_on_hold( - GetCurrencyId::get(), - reason, - source, - dest, - amount, - precision, - restriction, - fortitude, - ) - } -} - -impl fungible::UnbalancedHold for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - fn set_balance_on_hold(reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - as fungibles::UnbalancedHold<_>>::set_balance_on_hold(GetCurrencyId::get(), reason, who, amount) - } -} From b63c6ad23d0cfaf5afa5a4d4512ca8d8f4828892 Mon Sep 17 00:00:00 2001 From: wangjj9219 <183318287@qq.com> Date: Wed, 14 Jun 2023 18:49:04 +0800 Subject: [PATCH 02/11] add DustRemoval type to handle dust --- asset-registry/src/mock/para.rs | 1 + currencies/src/mock.rs | 3 +- payments/src/mock.rs | 1 + tokens/src/currency_adapter.rs | 483 ----------------------- tokens/src/impl_currency.rs | 9 +- tokens/src/impls.rs | 515 ++++++++++++++++++++++++- tokens/src/lib.rs | 78 +--- tokens/src/mock.rs | 16 +- traits/src/currency.rs | 6 +- xtokens/src/mock/para.rs | 1 + xtokens/src/mock/para_relative_view.rs | 1 + xtokens/src/mock/para_teleport.rs | 1 + 12 files changed, 551 insertions(+), 564 deletions(-) delete mode 100644 tokens/src/currency_adapter.rs diff --git a/asset-registry/src/mock/para.rs b/asset-registry/src/mock/para.rs index 25d1d4422..f50a6304a 100644 --- a/asset-registry/src/mock/para.rs +++ b/asset-registry/src/mock/para.rs @@ -101,6 +101,7 @@ impl orml_tokens::Config for Runtime { type MaxReserves = (); type MaxLocks = ConstU32<50>; type DustRemovalWhitelist = Nothing; + type DustRemoval = (); } #[derive(scale_info::TypeInfo, Encode, Decode, Clone, Eq, PartialEq, Debug)] diff --git a/currencies/src/mock.rs b/currencies/src/mock.rs index 9ae880892..41e2349be 100644 --- a/currencies/src/mock.rs +++ b/currencies/src/mock.rs @@ -82,7 +82,7 @@ impl MutationHooks, { - type OnDust = orml_tokens::TransferDust; + type OnDust = (); type OnSlash = (); type PreDeposit = (); type PostDeposit = (); @@ -104,6 +104,7 @@ impl orml_tokens::Config for Runtime { type MaxReserves = ConstU32<100_000>; type ReserveIdentifier = ReserveIdentifier; type DustRemovalWhitelist = Nothing; + type DustRemoval = (); } pub const NATIVE_CURRENCY_ID: CurrencyId = 1; diff --git a/payments/src/mock.rs b/payments/src/mock.rs index 7319c20cf..2269ea275 100644 --- a/payments/src/mock.rs +++ b/payments/src/mock.rs @@ -104,6 +104,7 @@ impl orml_tokens::Config for Test { type DustRemovalWhitelist = MockDustRemovalWhitelist; type MaxReserves = ConstU32<2>; type ReserveIdentifier = ReserveIdentifier; + type DustRemoval = (); } pub struct MockDisputeResolver; diff --git a/tokens/src/currency_adapter.rs b/tokens/src/currency_adapter.rs deleted file mode 100644 index 7b4aaf167..000000000 --- a/tokens/src/currency_adapter.rs +++ /dev/null @@ -1,483 +0,0 @@ -//! The adapter for specific token, which implements the -//! frame_support::traits::Currency traits, orml Currency traits and fungible -//! traits. - -use super::*; - -pub struct CurrencyAdapter(marker::PhantomData<(T, GetCurrencyId)>); - -impl PalletCurrency for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - type Balance = T::Balance; - type PositiveImbalance = PositiveImbalance; - type NegativeImbalance = NegativeImbalance; - - fn total_balance(who: &T::AccountId) -> Self::Balance { - as MultiCurrency<_>>::total_balance(GetCurrencyId::get(), who) - } - - fn can_slash(who: &T::AccountId, value: Self::Balance) -> bool { - as MultiCurrency<_>>::can_slash(GetCurrencyId::get(), who, value) - } - - fn total_issuance() -> Self::Balance { - as MultiCurrency<_>>::total_issuance(GetCurrencyId::get()) - } - - fn minimum_balance() -> Self::Balance { - as MultiCurrency<_>>::minimum_balance(GetCurrencyId::get()) - } - - fn burn(mut amount: Self::Balance) -> Self::PositiveImbalance { - if amount.is_zero() { - return PositiveImbalance::zero(); - } - let currency_id = GetCurrencyId::get(); - TotalIssuance::::mutate(currency_id, |issued| { - *issued = issued.checked_sub(&amount).unwrap_or_else(|| { - amount = *issued; - Zero::zero() - }) - }); - - Pallet::::deposit_event(Event::TotalIssuanceSet { - currency_id, - amount: Self::total_issuance(), - }); - PositiveImbalance::new(amount) - } - - fn issue(mut amount: Self::Balance) -> Self::NegativeImbalance { - if amount.is_zero() { - return NegativeImbalance::zero(); - } - TotalIssuance::::mutate(GetCurrencyId::get(), |issued| { - *issued = issued.checked_add(&amount).unwrap_or_else(|| { - amount = Self::Balance::max_value().defensive_saturating_sub(*issued); - Self::Balance::max_value() - }) - }); - - Pallet::::deposit_event(Event::TotalIssuanceSet { - currency_id: GetCurrencyId::get(), - amount: Self::total_issuance(), - }); - NegativeImbalance::new(amount) - } - - fn free_balance(who: &T::AccountId) -> Self::Balance { - as MultiCurrency<_>>::free_balance(GetCurrencyId::get(), who) - } - - fn ensure_can_withdraw( - who: &T::AccountId, - amount: Self::Balance, - _reasons: WithdrawReasons, - _new_balance: Self::Balance, - ) -> DispatchResult { - as MultiCurrency<_>>::ensure_can_withdraw(GetCurrencyId::get(), who, amount) - } - - fn transfer( - source: &T::AccountId, - dest: &T::AccountId, - value: Self::Balance, - existence_requirement: ExistenceRequirement, - ) -> DispatchResult { - Pallet::::do_transfer(GetCurrencyId::get(), source, dest, value, existence_requirement) - } - - fn slash(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { - if value.is_zero() { - return (Self::NegativeImbalance::zero(), value); - } - - let currency_id = GetCurrencyId::get(); - let account = Pallet::::accounts(who, currency_id); - let free_slashed_amount = account.free.min(value); - let mut remaining_slash = value.defensive_saturating_sub(free_slashed_amount); - - // slash free balance - if !free_slashed_amount.is_zero() { - Pallet::::set_free_balance( - currency_id, - who, - account.free.defensive_saturating_sub(free_slashed_amount), - ); - } - - // slash reserved balance - if !remaining_slash.is_zero() { - let reserved_slashed_amount = account.reserved.min(remaining_slash); - remaining_slash = remaining_slash.defensive_saturating_sub(reserved_slashed_amount); - Pallet::::set_reserved_balance( - currency_id, - who, - account.reserved.defensive_saturating_sub(reserved_slashed_amount), - ); - - Pallet::::deposit_event(Event::Slashed { - currency_id, - who: who.clone(), - free_amount: free_slashed_amount, - reserved_amount: reserved_slashed_amount, - }); - ( - Self::NegativeImbalance::new(free_slashed_amount.saturating_add(reserved_slashed_amount)), - remaining_slash, - ) - } else { - Pallet::::deposit_event(Event::Slashed { - currency_id, - who: who.clone(), - free_amount: value, - reserved_amount: Zero::zero(), - }); - (Self::NegativeImbalance::new(value), remaining_slash) - } - } - - /// Deposit some `value` into the free balance of an existing target account - /// `who`. - fn deposit_into_existing( - who: &T::AccountId, - value: Self::Balance, - ) -> sp_std::result::Result { - // do not change total issuance - Pallet::::do_deposit(GetCurrencyId::get(), who, value, true, false).map(|_| PositiveImbalance::new(value)) - } - - /// Deposit some `value` into the free balance of `who`, possibly creating a - /// new account. - fn deposit_creating(who: &T::AccountId, value: Self::Balance) -> Self::PositiveImbalance { - // do not change total issuance - Pallet::::do_deposit(GetCurrencyId::get(), who, value, false, false) - .map_or_else(|_| Self::PositiveImbalance::zero(), |_| PositiveImbalance::new(value)) - } - - fn withdraw( - who: &T::AccountId, - value: Self::Balance, - _reasons: WithdrawReasons, - liveness: ExistenceRequirement, - ) -> sp_std::result::Result { - // do not change total issuance - Pallet::::do_withdraw(GetCurrencyId::get(), who, value, liveness, false) - .map(|_| Self::NegativeImbalance::new(value)) - } - - fn make_free_balance_be( - who: &T::AccountId, - value: Self::Balance, - ) -> SignedImbalance { - let currency_id = GetCurrencyId::get(); - Pallet::::try_mutate_account( - currency_id, - who, - |account, is_new| -> Result, ()> { - // If we're attempting to set an existing account to less than ED, then - // bypass the entire operation. It's a no-op if you follow it through, but - // since this is an instance where we might account for a negative imbalance - // (in the dust cleaner of set_account) before we account for its actual - // equal and opposite cause (returned as an Imbalance), then in the - // instance that there's no other accounts on the system at all, we might - // underflow the issuance and our arithmetic will be off. - let ed = T::ExistentialDeposits::get(¤cy_id); - ensure!(value.saturating_add(account.reserved) >= ed || !is_new, ()); - - let imbalance = if account.free <= value { - SignedImbalance::Positive(PositiveImbalance::new(value.saturating_sub(account.free))) - } else { - SignedImbalance::Negative(NegativeImbalance::new(account.free.saturating_sub(value))) - }; - account.free = value; - - Pallet::::deposit_event(Event::BalanceSet { - currency_id, - who: who.clone(), - free: value, - reserved: account.reserved, - }); - Ok(imbalance) - }, - ) - .map(|(imbalance, _)| imbalance) - .unwrap_or_else(|_| SignedImbalance::Positive(Self::PositiveImbalance::zero())) - } -} - -impl PalletReservableCurrency for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - fn can_reserve(who: &T::AccountId, value: Self::Balance) -> bool { - as MultiReservableCurrency<_>>::can_reserve(GetCurrencyId::get(), who, value) - } - - fn slash_reserved(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { - let actual = as MultiReservableCurrency<_>>::slash_reserved(GetCurrencyId::get(), who, value); - (Self::NegativeImbalance::zero(), actual) - } - - fn reserved_balance(who: &T::AccountId) -> Self::Balance { - as MultiReservableCurrency<_>>::reserved_balance(GetCurrencyId::get(), who) - } - - fn reserve(who: &T::AccountId, value: Self::Balance) -> DispatchResult { - as MultiReservableCurrency<_>>::reserve(GetCurrencyId::get(), who, value) - } - - fn unreserve(who: &T::AccountId, value: Self::Balance) -> Self::Balance { - as MultiReservableCurrency<_>>::unreserve(GetCurrencyId::get(), who, value) - } - - fn repatriate_reserved( - slashed: &T::AccountId, - beneficiary: &T::AccountId, - value: Self::Balance, - status: Status, - ) -> sp_std::result::Result { - as MultiReservableCurrency<_>>::repatriate_reserved( - GetCurrencyId::get(), - slashed, - beneficiary, - value, - status, - ) - } -} - -impl PalletNamedReservableCurrency for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - type ReserveIdentifier = T::ReserveIdentifier; - - fn reserved_balance_named(id: &Self::ReserveIdentifier, who: &T::AccountId) -> Self::Balance { - as NamedMultiReservableCurrency<_>>::reserved_balance_named(id, GetCurrencyId::get(), who) - } - - fn reserve_named(id: &Self::ReserveIdentifier, who: &T::AccountId, value: Self::Balance) -> DispatchResult { - as NamedMultiReservableCurrency<_>>::reserve_named(id, GetCurrencyId::get(), who, value) - } - - fn unreserve_named(id: &Self::ReserveIdentifier, who: &T::AccountId, value: Self::Balance) -> Self::Balance { - as NamedMultiReservableCurrency<_>>::unreserve_named(id, GetCurrencyId::get(), who, value) - } - - fn slash_reserved_named( - id: &Self::ReserveIdentifier, - who: &T::AccountId, - value: Self::Balance, - ) -> (Self::NegativeImbalance, Self::Balance) { - let actual = - as NamedMultiReservableCurrency<_>>::slash_reserved_named(id, GetCurrencyId::get(), who, value); - (Self::NegativeImbalance::zero(), actual) - } - - fn repatriate_reserved_named( - id: &Self::ReserveIdentifier, - slashed: &T::AccountId, - beneficiary: &T::AccountId, - value: Self::Balance, - status: Status, - ) -> sp_std::result::Result { - as NamedMultiReservableCurrency<_>>::repatriate_reserved_named( - id, - GetCurrencyId::get(), - slashed, - beneficiary, - value, - status, - ) - } -} - -impl PalletLockableCurrency for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - type Moment = T::BlockNumber; - type MaxLocks = (); - - fn set_lock(id: LockIdentifier, who: &T::AccountId, amount: Self::Balance, _reasons: WithdrawReasons) { - let _ = as MultiLockableCurrency<_>>::set_lock(id, GetCurrencyId::get(), who, amount); - } - - fn extend_lock(id: LockIdentifier, who: &T::AccountId, amount: Self::Balance, _reasons: WithdrawReasons) { - let _ = as MultiLockableCurrency<_>>::extend_lock(id, GetCurrencyId::get(), who, amount); - } - - fn remove_lock(id: LockIdentifier, who: &T::AccountId) { - let _ = as MultiLockableCurrency<_>>::remove_lock(id, GetCurrencyId::get(), who); - } -} - -impl TransferAll for Pallet { - #[transactional] - fn transfer_all(source: &T::AccountId, dest: &T::AccountId) -> DispatchResult { - Accounts::::iter_prefix(source).try_for_each(|(currency_id, account_data)| -> DispatchResult { - // allow death - Self::do_transfer( - currency_id, - source, - dest, - account_data.free, - ExistenceRequirement::AllowDeath, - ) - }) - } -} - -impl fungible::Inspect for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - type Balance = T::Balance; - - fn total_issuance() -> Self::Balance { - as fungibles::Inspect<_>>::total_issuance(GetCurrencyId::get()) - } - fn minimum_balance() -> Self::Balance { - as fungibles::Inspect<_>>::minimum_balance(GetCurrencyId::get()) - } - fn balance(who: &T::AccountId) -> Self::Balance { - as fungibles::Inspect<_>>::balance(GetCurrencyId::get(), who) - } - fn total_balance(who: &T::AccountId) -> Self::Balance { - as fungibles::Inspect<_>>::total_balance(GetCurrencyId::get(), who) - } - fn reducible_balance(who: &T::AccountId, preservation: Preservation, fortitude: Fortitude) -> Self::Balance { - as fungibles::Inspect<_>>::reducible_balance(GetCurrencyId::get(), who, preservation, fortitude) - } - fn can_deposit(who: &T::AccountId, amount: Self::Balance, provenance: Provenance) -> DepositConsequence { - as fungibles::Inspect<_>>::can_deposit(GetCurrencyId::get(), who, amount, provenance) - } - fn can_withdraw(who: &T::AccountId, amount: Self::Balance) -> WithdrawConsequence { - as fungibles::Inspect<_>>::can_withdraw(GetCurrencyId::get(), who, amount) - } -} - -impl fungible::Mutate for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - fn mint_into(who: &T::AccountId, amount: Self::Balance) -> Result { - as fungibles::Mutate<_>>::mint_into(GetCurrencyId::get(), who, amount) - } - fn burn_from( - who: &T::AccountId, - amount: Self::Balance, - precision: Precision, - fortitude: Fortitude, - ) -> Result { - as fungibles::Mutate<_>>::burn_from(GetCurrencyId::get(), who, amount, precision, fortitude) - } - - fn transfer( - source: &T::AccountId, - dest: &T::AccountId, - amount: T::Balance, - preservation: Preservation, - ) -> Result { - as fungibles::Mutate<_>>::transfer(GetCurrencyId::get(), source, dest, amount, preservation) - } -} - -impl fungible::Unbalanced for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - fn handle_dust(_dust: fungible::Dust) { - // Dust is handled in account mutate method - } - - fn write_balance(who: &T::AccountId, amount: Self::Balance) -> Result, DispatchError> { - as fungibles::Unbalanced<_>>::write_balance(GetCurrencyId::get(), who, amount) - } - fn set_total_issuance(amount: Self::Balance) { - as fungibles::Unbalanced<_>>::set_total_issuance(GetCurrencyId::get(), amount) - } -} - -type ReasonOfFungible =

::AccountId>>::Reason; -impl fungible::InspectHold for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - type Reason = as fungibles::InspectHold>::Reason; - - fn balance_on_hold(reason: &Self::Reason, who: &T::AccountId) -> T::Balance { - as fungibles::InspectHold<_>>::balance_on_hold(GetCurrencyId::get(), reason, who) - } - fn total_balance_on_hold(who: &T::AccountId) -> Self::Balance { - as fungibles::InspectHold<_>>::total_balance_on_hold(GetCurrencyId::get(), who) - } - fn reducible_total_balance_on_hold(who: &T::AccountId, force: Fortitude) -> Self::Balance { - as fungibles::InspectHold<_>>::reducible_total_balance_on_hold(GetCurrencyId::get(), who, force) - } - fn hold_available(reason: &Self::Reason, who: &T::AccountId) -> bool { - as fungibles::InspectHold<_>>::hold_available(GetCurrencyId::get(), reason, who) - } - fn can_hold(reason: &Self::Reason, who: &T::AccountId, amount: T::Balance) -> bool { - as fungibles::InspectHold<_>>::can_hold(GetCurrencyId::get(), reason, who, amount) - } -} - -impl fungible::MutateHold for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - fn hold(reason: &ReasonOfFungible, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - as fungibles::MutateHold<_>>::hold(GetCurrencyId::get(), reason, who, amount) - } - fn release( - reason: &ReasonOfFungible, - who: &T::AccountId, - amount: Self::Balance, - precision: Precision, - ) -> Result { - as fungibles::MutateHold<_>>::release(GetCurrencyId::get(), reason, who, amount, precision) - } - fn transfer_on_hold( - reason: &ReasonOfFungible, - source: &T::AccountId, - dest: &T::AccountId, - amount: Self::Balance, - precision: Precision, - restriction: Restriction, - fortitude: Fortitude, - ) -> Result { - as fungibles::MutateHold<_>>::transfer_on_hold( - GetCurrencyId::get(), - reason, - source, - dest, - amount, - precision, - restriction, - fortitude, - ) - } -} - -impl fungible::UnbalancedHold for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - fn set_balance_on_hold(reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - as fungibles::UnbalancedHold<_>>::set_balance_on_hold(GetCurrencyId::get(), reason, who, amount) - } -} diff --git a/tokens/src/impl_currency.rs b/tokens/src/impl_currency.rs index cbab5d4aa..98b7117c3 100644 --- a/tokens/src/impl_currency.rs +++ b/tokens/src/impl_currency.rs @@ -1,8 +1,7 @@ // wrapping these imbalances in a private module is necessary to ensure absolute // privacy of the inner member. use super::*; -use frame_support::traits::{Get, Imbalance, SameOrOther, TryDrop}; -use sp_runtime::traits::{Saturating, Zero}; +use frame_support::traits::{SameOrOther, TryDrop}; use sp_std::{marker, mem, result}; /// Opaque, move-only struct with private fields that serves as a token @@ -430,7 +429,7 @@ impl MultiReservableCurrency for Pallet { ); let reserved_balance = Self::reserved_balance(currency_id, who); let actual = reserved_balance.min(value); - Self::mutate_account(currency_id, who, |account| { + Self::mutate_account_handling_dust(currency_id, who, |account| { // ensured reserved_balance >= actual but just to be defensive here. account.reserved = reserved_balance.defensive_saturating_sub(actual); }); @@ -459,7 +458,7 @@ impl MultiReservableCurrency for Pallet { } Self::ensure_can_withdraw(currency_id, who, value)?; - Self::mutate_account(currency_id, who, |account| { + Self::mutate_account_handling_dust(currency_id, who, |account| { account.free = account.free.defensive_saturating_sub(value); account.reserved = account.reserved.defensive_saturating_add(value); @@ -482,7 +481,7 @@ impl MultiReservableCurrency for Pallet { return value; } - let (remaining, _) = Self::mutate_account(currency_id, who, |account| { + let remaining = Self::mutate_account_handling_dust(currency_id, who, |account| { let actual = account.reserved.min(value); account.reserved = account.reserved.defensive_saturating_sub(actual); account.free = account.free.defensive_saturating_add(actual); diff --git a/tokens/src/impls.rs b/tokens/src/impls.rs index 9bbccc777..7ba9938b5 100644 --- a/tokens/src/impls.rs +++ b/tokens/src/impls.rs @@ -1,11 +1,31 @@ -use frame_support::dispatch::DispatchError; -use frame_support::traits::tokens::{Fortitude, Precision, Preservation, Provenance}; -use frame_support::traits::{ - fungible, fungibles, - tokens::{Balance as BalanceT, DepositConsequence, WithdrawConsequence}, - Contains, Get, +use super::*; +use frame_support::{ + traits::{ + tokens::{Balance as BalanceT, Precision, Restriction}, + Currency as PalletCurrency, LockableCurrency as PalletLockableCurrency, + NamedReservableCurrency as PalletNamedReservableCurrency, ReservableCurrency as PalletReservableCurrency, + SignedImbalance, WithdrawReasons, + }, + transactional, }; -use sp_arithmetic::{traits::Bounded, ArithmeticError}; + +pub type CreditOf = fungibles::Credit<::AccountId, Pallet>; +pub struct DustReceiver(sp_std::marker::PhantomData<(T, GetAccountId)>); +impl OnUnbalanced> for DustReceiver +where + T: Config, + GetAccountId: Get>, +{ + fn on_nonzero_unbalanced(amount: CreditOf) { + match GetAccountId::get() { + None => drop(amount), + Some(receiver) => { + let result = as fungibles::Balanced<_>>::resolve(&receiver, amount); + debug_assert!(result.is_ok()); + } + } + } +} pub struct Combiner(sp_std::marker::PhantomData<(AccountId, TestKey, A, B)>); @@ -344,3 +364,484 @@ where T::set_total_issuance(GetCurrencyId::get(), amount) } } + +// The adapter for specific token, which implements the +// frame_support::traits::Currency traits, orml Currency traits and fungible +// traits. +pub struct CurrencyAdapter(marker::PhantomData<(T, GetCurrencyId)>); + +impl PalletCurrency for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type Balance = T::Balance; + type PositiveImbalance = PositiveImbalance; + type NegativeImbalance = NegativeImbalance; + + fn total_balance(who: &T::AccountId) -> Self::Balance { + as MultiCurrency<_>>::total_balance(GetCurrencyId::get(), who) + } + + fn can_slash(who: &T::AccountId, value: Self::Balance) -> bool { + as MultiCurrency<_>>::can_slash(GetCurrencyId::get(), who, value) + } + + fn total_issuance() -> Self::Balance { + as MultiCurrency<_>>::total_issuance(GetCurrencyId::get()) + } + + fn minimum_balance() -> Self::Balance { + as MultiCurrency<_>>::minimum_balance(GetCurrencyId::get()) + } + + fn burn(mut amount: Self::Balance) -> Self::PositiveImbalance { + if amount.is_zero() { + return PositiveImbalance::zero(); + } + let currency_id = GetCurrencyId::get(); + TotalIssuance::::mutate(currency_id, |issued| { + *issued = issued.checked_sub(&amount).unwrap_or_else(|| { + amount = *issued; + Zero::zero() + }) + }); + + Pallet::::deposit_event(Event::TotalIssuanceSet { + currency_id, + amount: Self::total_issuance(), + }); + PositiveImbalance::new(amount) + } + + fn issue(mut amount: Self::Balance) -> Self::NegativeImbalance { + if amount.is_zero() { + return NegativeImbalance::zero(); + } + TotalIssuance::::mutate(GetCurrencyId::get(), |issued| { + *issued = issued.checked_add(&amount).unwrap_or_else(|| { + amount = Self::Balance::max_value().defensive_saturating_sub(*issued); + Self::Balance::max_value() + }) + }); + + Pallet::::deposit_event(Event::TotalIssuanceSet { + currency_id: GetCurrencyId::get(), + amount: Self::total_issuance(), + }); + NegativeImbalance::new(amount) + } + + fn free_balance(who: &T::AccountId) -> Self::Balance { + as MultiCurrency<_>>::free_balance(GetCurrencyId::get(), who) + } + + fn ensure_can_withdraw( + who: &T::AccountId, + amount: Self::Balance, + _reasons: WithdrawReasons, + _new_balance: Self::Balance, + ) -> DispatchResult { + as MultiCurrency<_>>::ensure_can_withdraw(GetCurrencyId::get(), who, amount) + } + + fn transfer( + source: &T::AccountId, + dest: &T::AccountId, + value: Self::Balance, + existence_requirement: ExistenceRequirement, + ) -> DispatchResult { + Pallet::::do_transfer(GetCurrencyId::get(), source, dest, value, existence_requirement) + } + + fn slash(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { + if value.is_zero() { + return (Self::NegativeImbalance::zero(), value); + } + + let currency_id = GetCurrencyId::get(); + let account = Pallet::::accounts(who, currency_id); + let free_slashed_amount = account.free.min(value); + let mut remaining_slash = value.defensive_saturating_sub(free_slashed_amount); + + // slash free balance + if !free_slashed_amount.is_zero() { + Pallet::::set_free_balance( + currency_id, + who, + account.free.defensive_saturating_sub(free_slashed_amount), + ); + } + + // slash reserved balance + if !remaining_slash.is_zero() { + let reserved_slashed_amount = account.reserved.min(remaining_slash); + remaining_slash = remaining_slash.defensive_saturating_sub(reserved_slashed_amount); + Pallet::::set_reserved_balance( + currency_id, + who, + account.reserved.defensive_saturating_sub(reserved_slashed_amount), + ); + + Pallet::::deposit_event(Event::Slashed { + currency_id, + who: who.clone(), + free_amount: free_slashed_amount, + reserved_amount: reserved_slashed_amount, + }); + ( + Self::NegativeImbalance::new(free_slashed_amount.saturating_add(reserved_slashed_amount)), + remaining_slash, + ) + } else { + Pallet::::deposit_event(Event::Slashed { + currency_id, + who: who.clone(), + free_amount: value, + reserved_amount: Zero::zero(), + }); + (Self::NegativeImbalance::new(value), remaining_slash) + } + } + + /// Deposit some `value` into the free balance of an existing target account + /// `who`. + fn deposit_into_existing( + who: &T::AccountId, + value: Self::Balance, + ) -> sp_std::result::Result { + // do not change total issuance + Pallet::::do_deposit(GetCurrencyId::get(), who, value, true, false).map(|_| PositiveImbalance::new(value)) + } + + /// Deposit some `value` into the free balance of `who`, possibly creating a + /// new account. + fn deposit_creating(who: &T::AccountId, value: Self::Balance) -> Self::PositiveImbalance { + // do not change total issuance + Pallet::::do_deposit(GetCurrencyId::get(), who, value, false, false) + .map_or_else(|_| Self::PositiveImbalance::zero(), |_| PositiveImbalance::new(value)) + } + + fn withdraw( + who: &T::AccountId, + value: Self::Balance, + _reasons: WithdrawReasons, + liveness: ExistenceRequirement, + ) -> sp_std::result::Result { + // do not change total issuance + Pallet::::do_withdraw(GetCurrencyId::get(), who, value, liveness, false) + .map(|_| Self::NegativeImbalance::new(value)) + } + + fn make_free_balance_be( + who: &T::AccountId, + value: Self::Balance, + ) -> SignedImbalance { + let currency_id = GetCurrencyId::get(); + Pallet::::try_mutate_account( + currency_id, + who, + |account, is_new| -> Result, ()> { + // If we're attempting to set an existing account to less than ED, then + // bypass the entire operation. It's a no-op if you follow it through, but + // since this is an instance where we might account for a negative imbalance + // (in the dust cleaner of set_account) before we account for its actual + // equal and opposite cause (returned as an Imbalance), then in the + // instance that there's no other accounts on the system at all, we might + // underflow the issuance and our arithmetic will be off. + let ed = T::ExistentialDeposits::get(¤cy_id); + ensure!(value.saturating_add(account.reserved) >= ed || !is_new, ()); + + let imbalance = if account.free <= value { + SignedImbalance::Positive(PositiveImbalance::new(value.saturating_sub(account.free))) + } else { + SignedImbalance::Negative(NegativeImbalance::new(account.free.saturating_sub(value))) + }; + account.free = value; + + Pallet::::deposit_event(Event::BalanceSet { + currency_id, + who: who.clone(), + free: value, + reserved: account.reserved, + }); + Ok(imbalance) + }, + ) + .map(|(imbalance, _)| imbalance) + .unwrap_or_else(|_| SignedImbalance::Positive(Self::PositiveImbalance::zero())) + } +} + +impl PalletReservableCurrency for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn can_reserve(who: &T::AccountId, value: Self::Balance) -> bool { + as MultiReservableCurrency<_>>::can_reserve(GetCurrencyId::get(), who, value) + } + + fn slash_reserved(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { + let actual = as MultiReservableCurrency<_>>::slash_reserved(GetCurrencyId::get(), who, value); + (Self::NegativeImbalance::zero(), actual) + } + + fn reserved_balance(who: &T::AccountId) -> Self::Balance { + as MultiReservableCurrency<_>>::reserved_balance(GetCurrencyId::get(), who) + } + + fn reserve(who: &T::AccountId, value: Self::Balance) -> DispatchResult { + as MultiReservableCurrency<_>>::reserve(GetCurrencyId::get(), who, value) + } + + fn unreserve(who: &T::AccountId, value: Self::Balance) -> Self::Balance { + as MultiReservableCurrency<_>>::unreserve(GetCurrencyId::get(), who, value) + } + + fn repatriate_reserved( + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: Status, + ) -> sp_std::result::Result { + as MultiReservableCurrency<_>>::repatriate_reserved( + GetCurrencyId::get(), + slashed, + beneficiary, + value, + status, + ) + } +} + +impl PalletNamedReservableCurrency for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type ReserveIdentifier = T::ReserveIdentifier; + + fn reserved_balance_named(id: &Self::ReserveIdentifier, who: &T::AccountId) -> Self::Balance { + as NamedMultiReservableCurrency<_>>::reserved_balance_named(id, GetCurrencyId::get(), who) + } + + fn reserve_named(id: &Self::ReserveIdentifier, who: &T::AccountId, value: Self::Balance) -> DispatchResult { + as NamedMultiReservableCurrency<_>>::reserve_named(id, GetCurrencyId::get(), who, value) + } + + fn unreserve_named(id: &Self::ReserveIdentifier, who: &T::AccountId, value: Self::Balance) -> Self::Balance { + as NamedMultiReservableCurrency<_>>::unreserve_named(id, GetCurrencyId::get(), who, value) + } + + fn slash_reserved_named( + id: &Self::ReserveIdentifier, + who: &T::AccountId, + value: Self::Balance, + ) -> (Self::NegativeImbalance, Self::Balance) { + let actual = + as NamedMultiReservableCurrency<_>>::slash_reserved_named(id, GetCurrencyId::get(), who, value); + (Self::NegativeImbalance::zero(), actual) + } + + fn repatriate_reserved_named( + id: &Self::ReserveIdentifier, + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: Status, + ) -> sp_std::result::Result { + as NamedMultiReservableCurrency<_>>::repatriate_reserved_named( + id, + GetCurrencyId::get(), + slashed, + beneficiary, + value, + status, + ) + } +} + +impl PalletLockableCurrency for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type Moment = T::BlockNumber; + type MaxLocks = (); + + fn set_lock(id: LockIdentifier, who: &T::AccountId, amount: Self::Balance, _reasons: WithdrawReasons) { + let _ = as MultiLockableCurrency<_>>::set_lock(id, GetCurrencyId::get(), who, amount); + } + + fn extend_lock(id: LockIdentifier, who: &T::AccountId, amount: Self::Balance, _reasons: WithdrawReasons) { + let _ = as MultiLockableCurrency<_>>::extend_lock(id, GetCurrencyId::get(), who, amount); + } + + fn remove_lock(id: LockIdentifier, who: &T::AccountId) { + let _ = as MultiLockableCurrency<_>>::remove_lock(id, GetCurrencyId::get(), who); + } +} + +impl TransferAll for Pallet { + #[transactional] + fn transfer_all(source: &T::AccountId, dest: &T::AccountId) -> DispatchResult { + Accounts::::iter_prefix(source).try_for_each(|(currency_id, account_data)| -> DispatchResult { + // allow death + Self::do_transfer( + currency_id, + source, + dest, + account_data.free, + ExistenceRequirement::AllowDeath, + ) + }) + } +} + +impl fungible::Inspect for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type Balance = T::Balance; + + fn total_issuance() -> Self::Balance { + as fungibles::Inspect<_>>::total_issuance(GetCurrencyId::get()) + } + fn minimum_balance() -> Self::Balance { + as fungibles::Inspect<_>>::minimum_balance(GetCurrencyId::get()) + } + fn balance(who: &T::AccountId) -> Self::Balance { + as fungibles::Inspect<_>>::balance(GetCurrencyId::get(), who) + } + fn total_balance(who: &T::AccountId) -> Self::Balance { + as fungibles::Inspect<_>>::total_balance(GetCurrencyId::get(), who) + } + fn reducible_balance(who: &T::AccountId, preservation: Preservation, fortitude: Fortitude) -> Self::Balance { + as fungibles::Inspect<_>>::reducible_balance(GetCurrencyId::get(), who, preservation, fortitude) + } + fn can_deposit(who: &T::AccountId, amount: Self::Balance, provenance: Provenance) -> DepositConsequence { + as fungibles::Inspect<_>>::can_deposit(GetCurrencyId::get(), who, amount, provenance) + } + fn can_withdraw(who: &T::AccountId, amount: Self::Balance) -> WithdrawConsequence { + as fungibles::Inspect<_>>::can_withdraw(GetCurrencyId::get(), who, amount) + } +} + +impl fungible::Mutate for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn mint_into(who: &T::AccountId, amount: Self::Balance) -> Result { + as fungibles::Mutate<_>>::mint_into(GetCurrencyId::get(), who, amount) + } + fn burn_from( + who: &T::AccountId, + amount: Self::Balance, + precision: Precision, + fortitude: Fortitude, + ) -> Result { + as fungibles::Mutate<_>>::burn_from(GetCurrencyId::get(), who, amount, precision, fortitude) + } + + fn transfer( + source: &T::AccountId, + dest: &T::AccountId, + amount: T::Balance, + preservation: Preservation, + ) -> Result { + as fungibles::Mutate<_>>::transfer(GetCurrencyId::get(), source, dest, amount, preservation) + } +} + +impl fungible::Unbalanced for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn handle_dust(_dust: fungible::Dust) { + // Dust is handled in account mutate method + } + + fn write_balance(who: &T::AccountId, amount: Self::Balance) -> Result, DispatchError> { + as fungibles::Unbalanced<_>>::write_balance(GetCurrencyId::get(), who, amount) + } + fn set_total_issuance(amount: Self::Balance) { + as fungibles::Unbalanced<_>>::set_total_issuance(GetCurrencyId::get(), amount) + } +} + +type ReasonOfFungible =

::AccountId>>::Reason; +impl fungible::InspectHold for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type Reason = as fungibles::InspectHold>::Reason; + + fn balance_on_hold(reason: &Self::Reason, who: &T::AccountId) -> T::Balance { + as fungibles::InspectHold<_>>::balance_on_hold(GetCurrencyId::get(), reason, who) + } + fn total_balance_on_hold(who: &T::AccountId) -> Self::Balance { + as fungibles::InspectHold<_>>::total_balance_on_hold(GetCurrencyId::get(), who) + } + fn reducible_total_balance_on_hold(who: &T::AccountId, force: Fortitude) -> Self::Balance { + as fungibles::InspectHold<_>>::reducible_total_balance_on_hold(GetCurrencyId::get(), who, force) + } + fn hold_available(reason: &Self::Reason, who: &T::AccountId) -> bool { + as fungibles::InspectHold<_>>::hold_available(GetCurrencyId::get(), reason, who) + } + fn can_hold(reason: &Self::Reason, who: &T::AccountId, amount: T::Balance) -> bool { + as fungibles::InspectHold<_>>::can_hold(GetCurrencyId::get(), reason, who, amount) + } +} + +impl fungible::MutateHold for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn hold(reason: &ReasonOfFungible, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + as fungibles::MutateHold<_>>::hold(GetCurrencyId::get(), reason, who, amount) + } + fn release( + reason: &ReasonOfFungible, + who: &T::AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + as fungibles::MutateHold<_>>::release(GetCurrencyId::get(), reason, who, amount, precision) + } + fn transfer_on_hold( + reason: &ReasonOfFungible, + source: &T::AccountId, + dest: &T::AccountId, + amount: Self::Balance, + precision: Precision, + restriction: Restriction, + fortitude: Fortitude, + ) -> Result { + as fungibles::MutateHold<_>>::transfer_on_hold( + GetCurrencyId::get(), + reason, + source, + dest, + amount, + precision, + restriction, + fortitude, + ) + } +} + +impl fungible::UnbalancedHold for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn set_balance_on_hold(reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + as fungibles::UnbalancedHold<_>>::set_balance_on_hold(GetCurrencyId::get(), reason, who, amount) + } +} diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index b49bb6aac..579ee95b2 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -43,16 +43,10 @@ use frame_support::{ ensure, log, pallet_prelude::*, traits::{ - tokens::{ - fungible, fungibles, DepositConsequence, Fortitude, Precision, Preservation, Provenance, Restriction, - WithdrawConsequence, - }, - BalanceStatus as Status, Contains, Currency as PalletCurrency, DefensiveSaturating, ExistenceRequirement, Get, - Imbalance, LockableCurrency as PalletLockableCurrency, - NamedReservableCurrency as PalletNamedReservableCurrency, OnUnbalanced, - ReservableCurrency as PalletReservableCurrency, SignedImbalance, WithdrawReasons, + tokens::{fungible, fungibles, DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence}, + BalanceStatus as Status, Contains, DefensiveSaturating, ExistenceRequirement, Get, Imbalance, OnUnbalanced, }, - transactional, BoundedVec, + BoundedVec, }; use frame_system::{ensure_signed, pallet_prelude::*}; use scale_info::TypeInfo; @@ -72,52 +66,22 @@ use orml_traits::{ MultiReservableCurrency, NamedMultiReservableCurrency, }; -mod currency_adapter; mod impl_currency; mod impl_fungibles; mod impls; mod mock; -mod tests; -mod tests_currency_adapter; -mod tests_events; -mod tests_fungibles; -mod tests_multicurrency; +// mod tests; +// mod tests_currency_adapter; +// mod tests_events; +// mod tests_fungibles; +// mod tests_multicurrency; mod weights; -pub use currency_adapter::CurrencyAdapter; pub use impl_currency::{NegativeImbalance, PositiveImbalance}; pub use impls::*; pub use weights::WeightInfo; -pub struct TransferDust(marker::PhantomData<(T, GetAccountId)>); -impl OnDust for TransferDust -where - T: Config, - GetAccountId: Get, -{ - fn on_dust(who: &T::AccountId, currency_id: T::CurrencyId, amount: T::Balance) { - // transfer the dust to treasury account, ignore the result, - // if failed will leave some dust which still could be recycled. - let _ = Pallet::::do_transfer( - currency_id, - who, - &GetAccountId::get(), - amount, - ExistenceRequirement::AllowDeath, - ); - } -} - -pub struct BurnDust(marker::PhantomData); -impl OnDust for BurnDust { - fn on_dust(who: &T::AccountId, currency_id: T::CurrencyId, amount: T::Balance) { - // burn the dust, ignore the result, - // if failed will leave some dust which still could be recycled. - let _ = Pallet::::do_withdraw(currency_id, who, amount, ExistenceRequirement::AllowDeath, true); - } -} - /// A single lock on a balance. There can be many of these on an account and /// they "overlap", so the same balance is frozen by multiple locks. #[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)] @@ -181,25 +145,6 @@ pub mod module { use super::*; - pub type CreditOf = fungibles::Credit<::AccountId, Pallet>; - - pub struct DustReceiver(sp_std::marker::PhantomData<(T, GetAccountId)>); - impl OnUnbalanced> for DustReceiver - where - T: Config, - GetAccountId: Get>, - { - fn on_nonzero_unbalanced(amount: CreditOf) { - match GetAccountId::get() { - None => drop(amount), - Some(receiver) => { - let result = as fungibles::Balanced<_>>::resolve(&receiver, amount); - debug_assert!(result.is_ok()); - } - } - } - } - #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; @@ -466,7 +411,7 @@ pub mod module { *initial_balance >= T::ExistentialDeposits::get(currency_id), "the balance of any account should always be more than existential deposit.", ); - Pallet::::mutate_account(account_id, *currency_id, |account_data| { + Pallet::::mutate_account(*currency_id, account_id, |account_data| { account_data.free = *initial_balance }); TotalIssuance::::mutate(*currency_id, |total_issuance| { @@ -782,6 +727,11 @@ impl Pallet { } if let Some(dust_amount) = maybe_dust { + >::OnDust::on_dust( + currency_id, + who, + dust_amount, + ); Self::deposit_event(Event::DustLost { currency_id, who: who.clone(), diff --git a/tokens/src/mock.rs b/tokens/src/mock.rs index ef646f13c..198e7144f 100644 --- a/tokens/src/mock.rs +++ b/tokens/src/mock.rs @@ -294,6 +294,7 @@ where } thread_local! { + pub static ON_DUST_CALLS: RefCell = RefCell::new(0); pub static ON_SLASH_CALLS: RefCell = RefCell::new(0); pub static ON_DEPOSIT_PREHOOK_CALLS: RefCell = RefCell::new(0); pub static ON_DEPOSIT_POSTHOOK_CALLS: RefCell = RefCell::new(0); @@ -301,6 +302,18 @@ thread_local! { pub static ON_TRANSFER_POSTHOOK_CALLS: RefCell = RefCell::new(0); } +pub struct OnDustHook(marker::PhantomData); +impl OnDust for OnDustHook { + fn on_dust(_currency_id: T::CurrencyId, _account_id: &T::AccountId, _amount: T::Balance) { + ON_DUST_CALLS.with(|cell| *cell.borrow_mut() += 1); + } +} +impl OnDustHook { + pub fn calls() -> u32 { + ON_SLASH_CALLS.with(|accounts| *accounts.borrow()) + } +} + pub struct OnSlashHook(marker::PhantomData); impl OnSlash for OnSlashHook { fn on_slash(_currency_id: T::CurrencyId, _account_id: &T::AccountId, _amount: T::Balance) { @@ -397,7 +410,7 @@ where T::AccountId: From + Into, T::CurrencyId: From + Into, { - type OnDust = TransferDust; + type OnDust = OnDustHook; type OnSlash = OnSlashHook; type PreDeposit = PreDeposit; type PostDeposit = PostDeposit; @@ -419,6 +432,7 @@ impl Config for Runtime { type MaxReserves = ConstU32<2>; type ReserveIdentifier = ReserveIdentifier; type DustRemovalWhitelist = MockDustRemovalWhitelist; + type DustRemoval = (); } pub type TreasuryCurrencyAdapter = ::Currency; diff --git a/traits/src/currency.rs b/traits/src/currency.rs index 0d50e7477..8893a400e 100644 --- a/traits/src/currency.rs +++ b/traits/src/currency.rs @@ -638,11 +638,11 @@ pub trait NamedBasicReservableCurrency: BasicReser /// Handler for account which has dust, need to burn or recycle it pub trait OnDust { - fn on_dust(who: &AccountId, currency_id: CurrencyId, amount: Balance); + fn on_dust(currency_id: CurrencyId, who: &AccountId, amount: Balance); } impl OnDust for () { - fn on_dust(_: &AccountId, _: CurrencyId, _: Balance) {} + fn on_dust(_: CurrencyId, _: &AccountId, _: Balance) {} } pub trait TransferAll { @@ -692,7 +692,7 @@ impl OnTransfer } pub trait MutationHooks { - /// Handler to burn or transfer account's dust. + /// Handler to when account's dust lost. type OnDust: OnDust; /// Hook to run before slashing an account. diff --git a/xtokens/src/mock/para.rs b/xtokens/src/mock/para.rs index ea774d339..7c679fb1a 100644 --- a/xtokens/src/mock/para.rs +++ b/xtokens/src/mock/para.rs @@ -92,6 +92,7 @@ impl orml_tokens::Config for Runtime { type MaxReserves = ConstU32<50>; type ReserveIdentifier = [u8; 8]; type DustRemovalWhitelist = Everything; + type DustRemoval = (); } parameter_types! { diff --git a/xtokens/src/mock/para_relative_view.rs b/xtokens/src/mock/para_relative_view.rs index 66b779d03..fb22619fd 100644 --- a/xtokens/src/mock/para_relative_view.rs +++ b/xtokens/src/mock/para_relative_view.rs @@ -95,6 +95,7 @@ impl orml_tokens::Config for Runtime { type MaxReserves = ConstU32<50>; type ReserveIdentifier = [u8; 8]; type DustRemovalWhitelist = Everything; + type DustRemoval = (); } parameter_types! { diff --git a/xtokens/src/mock/para_teleport.rs b/xtokens/src/mock/para_teleport.rs index f4c4c41d8..ad74787b5 100644 --- a/xtokens/src/mock/para_teleport.rs +++ b/xtokens/src/mock/para_teleport.rs @@ -93,6 +93,7 @@ impl orml_tokens::Config for Runtime { type MaxReserves = ConstU32<50>; type ReserveIdentifier = [u8; 8]; type DustRemovalWhitelist = Everything; + type DustRemoval = (); } parameter_types! { From e3b2a04267d3bae3d3998c3639710fb420366d78 Mon Sep 17 00:00:00 2001 From: wangjj9219 <183318287@qq.com> Date: Wed, 14 Jun 2023 23:51:46 +0800 Subject: [PATCH 03/11] overwrite Unbalanced::increase_balance --- tokens/src/impl_fungibles.rs | 42 +++++++ tokens/src/impls.rs | 2 +- tokens/src/lib.rs | 17 +-- tokens/src/mock.rs | 26 +++- tokens/src/tests.rs | 238 +++-------------------------------- 5 files changed, 88 insertions(+), 237 deletions(-) diff --git a/tokens/src/impl_fungibles.rs b/tokens/src/impl_fungibles.rs index b0e8d1494..8f743cec7 100644 --- a/tokens/src/impl_fungibles.rs +++ b/tokens/src/impl_fungibles.rs @@ -190,6 +190,48 @@ impl fungibles::Unbalanced for Pallet { // Balance is the same type and will not overflow TotalIssuance::::mutate(asset_id, |t| *t = amount); } + + /// Increase the balance of `who` by `amount`. + /// + /// If it cannot be increased by that amount for some reason, return `Err` + /// and don't increase it at all. If Ok, return the imbalance. + /// Minimum balance will be respected and an error will be returned if + /// `amount < Self::minimum_balance()` when the account of `who` is zero. + /// NOTE: this impl overrides the default implementation of + /// fungibles::Unbalanced, allow `amount < Self::minimum_balance() && who is + /// in DustRemovalWhitelist` when the account of `who` is zero + fn increase_balance( + asset: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + let old_balance = >::balance(asset, who); + let new_balance = if let Precision::BestEffort = precision { + old_balance.saturating_add(amount) + } else { + old_balance.checked_add(&amount).ok_or(ArithmeticError::Overflow)? + }; + if new_balance < >::minimum_balance(asset) + && !Self::in_dust_removal_whitelist(who) + { + // Attempt to increase from 0 to below minimum -> stays at zero. + if let Precision::BestEffort = precision { + Ok(Self::Balance::default()) + } else { + Err(TokenError::BelowMinimum.into()) + } + } else { + if new_balance == old_balance { + Ok(Self::Balance::default()) + } else { + if let Some(dust) = Self::write_balance(asset, who, new_balance)? { + Self::handle_dust(fungibles::Dust(asset, dust)); + } + Ok(new_balance.saturating_sub(old_balance)) + } + } + } } impl fungibles::Balanced for Pallet { diff --git a/tokens/src/impls.rs b/tokens/src/impls.rs index 7ba9938b5..a532d452a 100644 --- a/tokens/src/impls.rs +++ b/tokens/src/impls.rs @@ -1,7 +1,7 @@ use super::*; use frame_support::{ traits::{ - tokens::{Balance as BalanceT, Precision, Restriction}, + tokens::{Balance as BalanceT, Restriction}, Currency as PalletCurrency, LockableCurrency as PalletLockableCurrency, NamedReservableCurrency as PalletNamedReservableCurrency, ReservableCurrency as PalletReservableCurrency, SignedImbalance, WithdrawReasons, diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index 579ee95b2..43337fa86 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -43,7 +43,10 @@ use frame_support::{ ensure, log, pallet_prelude::*, traits::{ - tokens::{fungible, fungibles, DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence}, + tokens::{ + fungible, fungibles, DepositConsequence, Fortitude, Precision, Preservation, Provenance, + WithdrawConsequence, + }, BalanceStatus as Status, Contains, DefensiveSaturating, ExistenceRequirement, Get, Imbalance, OnUnbalanced, }, BoundedVec, @@ -55,7 +58,7 @@ use sp_runtime::{ AtLeast32BitUnsigned, Bounded, CheckedAdd, CheckedSub, MaybeSerializeDeserialize, Member, Saturating, StaticLookup, Zero, }, - ArithmeticError, DispatchError, DispatchResult, FixedPointOperand, RuntimeDebug, + ArithmeticError, DispatchError, DispatchResult, FixedPointOperand, RuntimeDebug, TokenError, }; use sp_std::{cmp, convert::Infallible, marker, prelude::*, vec::Vec}; @@ -70,11 +73,11 @@ mod impl_currency; mod impl_fungibles; mod impls; mod mock; -// mod tests; -// mod tests_currency_adapter; -// mod tests_events; -// mod tests_fungibles; -// mod tests_multicurrency; +mod tests; +mod tests_currency_adapter; +mod tests_events; +mod tests_fungibles; +mod tests_multicurrency; mod weights; diff --git a/tokens/src/mock.rs b/tokens/src/mock.rs index 198e7144f..efdcf73a5 100644 --- a/tokens/src/mock.rs +++ b/tokens/src/mock.rs @@ -209,7 +209,7 @@ impl pallet_elections_phragmen::Config for Runtime { pub struct MockDustRemovalWhitelist; impl Contains for MockDustRemovalWhitelist { fn contains(a: &AccountId) -> bool { - *a == DAVE || *a == DustReceiver::get() + *a == DAVE || *a == DustReceiverAccount::get() } } @@ -400,10 +400,6 @@ impl PostTransfer { } } -parameter_types! { - pub DustReceiver: AccountId = PalletId(*b"orml/dst").into_account_truncating(); -} - pub struct CurrencyHooks(marker::PhantomData); impl MutationHooks for CurrencyHooks where @@ -420,6 +416,24 @@ where type OnKilledTokenAccount = TrackKilledAccounts; } +parameter_types! { + pub DustReceiverAccount: AccountId = PalletId(*b"orml/dst").into_account_truncating(); + pub static GetDustReceiverAccount: Option = Some(DustReceiverAccount::get()); +} + +pub struct MockDustRemoval; +impl OnUnbalanced> for MockDustRemoval { + fn on_nonzero_unbalanced(amount: fungibles::Credit) { + match GetDustReceiverAccount::get() { + None => drop(amount), + Some(a) => { + let result = >::resolve(&a, amount); + debug_assert!(result.is_ok()); + } + } + } +} + impl Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = Balance; @@ -432,7 +446,7 @@ impl Config for Runtime { type MaxReserves = ConstU32<2>; type ReserveIdentifier = ReserveIdentifier; type DustRemovalWhitelist = MockDustRemovalWhitelist; - type DustRemoval = (); + type DustRemoval = MockDustRemoval; } pub type TreasuryCurrencyAdapter = ::Currency; diff --git a/tokens/src/tests.rs b/tokens/src/tests.rs index 504ca06ab..d724de82d 100644 --- a/tokens/src/tests.rs +++ b/tokens/src/tests.rs @@ -20,7 +20,7 @@ fn genesis_issuance_should_work() { .execute_with(|| { assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); assert_eq!(Tokens::free_balance(DOT, &BOB), 100); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 0); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 0); assert_eq!(Tokens::total_issuance(DOT), 200); }); } @@ -268,213 +268,6 @@ fn set_balance_should_work() { // tests for inline impl // ************************************************* -#[test] -fn deposit_consequence_should_work() { - ExtBuilder::default().build().execute_with(|| { - assert_eq!( - Tokens::deposit_consequence( - &CHARLIE, - DOT, - 0, - &AccountData { - free: 1, - reserved: 0, - frozen: 0 - } - ) - .into_result(), - Ok(()) - ); - - // total issuance overflow - assert_eq!( - Tokens::deposit_consequence( - &CHARLIE, - DOT, - Balance::max_value(), - &AccountData { - free: 1, - reserved: 0, - frozen: 0 - } - ) - .into_result(), - Err(ArithmeticError::Overflow.into()) - ); - - // total balance overflow - assert_eq!( - Tokens::deposit_consequence( - &CHARLIE, - DOT, - 1, - &AccountData { - free: Balance::max_value(), - reserved: 0, - frozen: 0 - } - ) - .into_result(), - Err(ArithmeticError::Overflow.into()) - ); - - // below ed - assert_eq!( - Tokens::deposit_consequence( - &CHARLIE, - DOT, - 1, - &AccountData { - free: 0, - reserved: 0, - frozen: 0 - } - ) - .into_result(), - Err(TokenError::BelowMinimum.into()) - ); - - assert_eq!( - Tokens::deposit_consequence( - &CHARLIE, - DOT, - 1, - &AccountData { - free: 1, - reserved: 0, - frozen: 0 - } - ) - .into_result(), - Ok(()) - ); - }); -} - -#[test] -fn withdraw_consequence_should_work() { - ExtBuilder::default().build().execute_with(|| { - assert_eq!( - Tokens::withdraw_consequence( - &ALICE, - DOT, - 0, - &AccountData { - free: 1, - reserved: 0, - frozen: 0 - } - ) - .into_result(true), - Ok(Zero::zero()) - ); - - // total issuance underflow - assert_ok!(Tokens::update_balance(DOT, &ALICE, 2)); - assert_eq!(Tokens::total_issuance(DOT), 2); - assert_eq!( - Tokens::withdraw_consequence( - &ALICE, - DOT, - 3, - &AccountData { - free: 1, - reserved: 0, - frozen: 0 - } - ) - .into_result(true), - Err(ArithmeticError::Underflow.into()) - ); - - // total issuance is not enough - assert_eq!( - Tokens::withdraw_consequence( - &ALICE, - DOT, - 2, - &AccountData { - free: 1, - reserved: 0, - frozen: 0 - } - ) - .into_result(true), - Err(TokenError::FundsUnavailable.into()) - ); - - // below ED and cannot dec provider - assert_ok!(Tokens::update_balance(DOT, &ALICE, 2)); - assert_eq!(System::providers(&ALICE), 1); - assert_ok!(System::inc_consumers(&ALICE)); - assert!(!System::can_dec_provider(&ALICE)); - assert_eq!( - Tokens::withdraw_consequence( - &ALICE, - DOT, - 1, - &AccountData { - free: 2, - reserved: 0, - frozen: 0 - } - ) - .into_result(true), - Err(TokenError::OnlyProvider.into()) - ); - - // below ED and can dec provider - let _ = System::inc_providers(&ALICE); - assert!(System::can_dec_provider(&ALICE)); - assert_eq!( - Tokens::withdraw_consequence( - &ALICE, - DOT, - 1, - &AccountData { - free: 2, - reserved: 0, - frozen: 0 - } - ) - .into_result(false), - Ok(1) - ); - - // free balance is not enough - assert_eq!( - Tokens::withdraw_consequence( - &ALICE, - DOT, - 2, - &AccountData { - free: 1, - reserved: 1, - frozen: 0 - } - ) - .into_result(true), - Err(TokenError::FundsUnavailable.into()) - ); - - // less to frozen balance - assert_eq!( - Tokens::withdraw_consequence( - &ALICE, - DOT, - 2, - &AccountData { - free: 2, - reserved: 0, - frozen: 2 - } - ) - .into_result(true), - Err(TokenError::Frozen.into()) - ); - }); -} - #[test] fn ensure_can_withdraw_should_work() { ExtBuilder::default() @@ -504,26 +297,25 @@ fn set_free_balance_should_work() { /* Scenarios: ED is not zero, account is not in dust removal whitelist */ assert!(!Accounts::::contains_key(ALICE, DOT)); assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 0); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 0); assert_eq!(Tokens::total_issuance(DOT), 0); // when total is below ED, account will be reaped. Tokens::set_free_balance(DOT, &ALICE, 1); - assert!(!Accounts::::contains_key(ALICE, DOT)); assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 1); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 1); // set_free_balance do not change total issuance. assert_eq!(Tokens::total_issuance(DOT), 0); Tokens::set_free_balance(DOT, &ALICE, 2); assert!(Accounts::::contains_key(ALICE, DOT)); assert_eq!(Tokens::free_balance(DOT, &ALICE), 2); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 1); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 1); /* Scenarios: ED is not zero, account is in dust removal whitelist */ assert!(!Accounts::::contains_key(DAVE, DOT)); assert_eq!(Tokens::free_balance(DOT, &DAVE), 0); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 1); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 1); // set zero will not create account Tokens::set_free_balance(DOT, &DAVE, 0); @@ -533,18 +325,18 @@ fn set_free_balance_should_work() { Tokens::set_free_balance(DOT, &DAVE, 1); assert!(Accounts::::contains_key(DAVE, DOT)); assert_eq!(Tokens::free_balance(DOT, &DAVE), 1); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 1); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 1); /* Scenarios: ED is zero */ assert!(!Accounts::::contains_key(ALICE, ETH)); assert_eq!(Tokens::free_balance(ETH, &ALICE), 0); - assert_eq!(Tokens::free_balance(ETH, &DustReceiver::get()), 0); + assert_eq!(Tokens::free_balance(ETH, &DustReceiverAccount::get()), 0); // set zero will create account Tokens::set_free_balance(ETH, &ALICE, 0); assert!(Accounts::::contains_key(ALICE, ETH)); assert_eq!(Tokens::free_balance(ETH, &ALICE), 0); - assert_eq!(Tokens::free_balance(ETH, &DustReceiver::get()), 0); + assert_eq!(Tokens::free_balance(ETH, &DustReceiverAccount::get()), 0); }); } @@ -640,7 +432,7 @@ fn do_transfer_dust_removal_when_allow_death() { .execute_with(|| { assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); assert_eq!(Tokens::free_balance(DOT, &BOB), 100); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 0); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 0); assert_ok!(Tokens::do_transfer( DOT, @@ -651,7 +443,7 @@ fn do_transfer_dust_removal_when_allow_death() { )); assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); assert_eq!(Tokens::free_balance(DOT, &BOB), 199); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 1); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 1); }); } @@ -837,7 +629,7 @@ fn do_withdraw_dust_removal_when_allow_death() { assert_eq!(Tokens::total_issuance(DOT), 100); assert!(Accounts::::contains_key(ALICE, DOT)); assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 0); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 0); assert_ok!(Tokens::do_withdraw( DOT, @@ -849,7 +641,7 @@ fn do_withdraw_dust_removal_when_allow_death() { assert_eq!(Tokens::total_issuance(DOT), 1); assert!(!Accounts::::contains_key(ALICE, DOT)); assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 1); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 1); }); } @@ -1082,7 +874,7 @@ fn dust_removal_work() { assert_eq!(System::providers(&ALICE), 1); assert!(Accounts::::contains_key(ALICE, DOT)); assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 0); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 0); Tokens::set_free_balance(DOT, &ALICE, 1); System::assert_last_event(RuntimeEvent::Tokens(crate::Event::DustLost { currency_id: DOT, @@ -1092,7 +884,7 @@ fn dust_removal_work() { assert_eq!(System::providers(&ALICE), 0); assert!(!Accounts::::contains_key(ALICE, DOT)); assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 1); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 1); // dave is in dust removal whitelist, will not remove its dust even if its total // below ED @@ -1114,7 +906,7 @@ fn dust_removal_work() { #[test] fn account_survive_due_to_dust_transfer_failure() { ExtBuilder::default().build().execute_with(|| { - let dust_account = DustReceiver::get(); + let dust_account = DustReceiverAccount::get(); Tokens::set_free_balance(DOT, &dust_account, 0); assert_eq!(Tokens::free_balance(DOT, &dust_account), 0); assert_eq!(Tokens::total_balance(DOT, &ALICE), 0); From a3f0e464ed22a7ee73af3ce0541069b63cbc5751 Mon Sep 17 00:00:00 2001 From: wangjj9219 <183318287@qq.com> Date: Thu, 15 Jun 2023 23:08:53 +0800 Subject: [PATCH 04/11] add TODO --- tokens/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index 43337fa86..bc6abd77f 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -707,6 +707,8 @@ impl Pallet { let exists = maybe_account.is_some(); + // TODO: need review new provider and consumer machanism of frame_system to decide + // how to handle the providers here! if !is_new && !exists { // If existed before, decrease account provider. // Ignore the result, because if it failed then there are remaining consumers, From b8bfa65b72633e65f8888e2a74bd0265696d36b7 Mon Sep 17 00:00:00 2001 From: wangjj9219 <183318287@qq.com> Date: Wed, 9 Aug 2023 22:32:22 +0800 Subject: [PATCH 05/11] remove set_free_balance and set_reserved_balance --- tokens/src/impl_currency.rs | 157 +++++++------------ tokens/src/impl_fungibles.rs | 42 ----- tokens/src/impls.rs | 61 +++----- tokens/src/lib.rs | 293 ++++++++++++++++++----------------- 4 files changed, 224 insertions(+), 329 deletions(-) diff --git a/tokens/src/impl_currency.rs b/tokens/src/impl_currency.rs index 757b11b3c..bd21287ee 100644 --- a/tokens/src/impl_currency.rs +++ b/tokens/src/impl_currency.rs @@ -230,11 +230,7 @@ impl MultiCurrency for Pallet { /// Is a no-op if `value` to be slashed is zero. /// - /// NOTE: `slash()` prefers free balance, but assumes that reserve - /// balance can be drawn from in extreme circumstances. `can_slash()` - /// should be used prior to `slash()` to avoid having to draw from - /// reserved funds, however we err on the side of punishment if things - /// are inconsistent or `can_slash` wasn't used appropriately. + /// NOTE: `slash()` just slash free balance. fn slash(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> Self::Balance { if amount.is_zero() { return amount; @@ -245,48 +241,24 @@ impl MultiCurrency for Pallet { who, amount, ); - let account = Self::accounts(who, currency_id); - let free_slashed_amount = account.free.min(amount); - // Cannot underflow because free_slashed_amount can never be greater than amount - // but just to be defensive here. - let mut remaining_slash = amount.defensive_saturating_sub(free_slashed_amount); - - // slash free balance - if !free_slashed_amount.is_zero() { - // Cannot underflow becuase free_slashed_amount can never be greater than - // account.free but just to be defensive here. - Self::set_free_balance( - currency_id, - who, - account.free.defensive_saturating_sub(free_slashed_amount), - ); - } + let remaining_slash = Self::mutate_account_handling_dust(currency_id, who, |account| -> Self::Balance { + let free_slashed_amount = account.free.min(amount); + account.free = account.free.defensive_saturating_sub(free_slashed_amount); - // slash reserved balance - let reserved_slashed_amount = account.reserved.min(remaining_slash); + // Cannot underflow because the slashed value cannot be greater than total + // issuance but just to be defensive here. + TotalIssuance::::mutate(currency_id, |v| *v = v.defensive_saturating_sub(free_slashed_amount)); - if !reserved_slashed_amount.is_zero() { - // Cannot underflow due to above line but just to be defensive here. - remaining_slash = remaining_slash.defensive_saturating_sub(reserved_slashed_amount); - Self::set_reserved_balance( + Self::deposit_event(Event::Slashed { currency_id, - who, - account.reserved.defensive_saturating_sub(reserved_slashed_amount), - ); - } + who: who.clone(), + free_amount: free_slashed_amount, + reserved_amount: Zero::zero(), + }); - // Cannot underflow because the slashed value cannot be greater than total - // issuance but just to be defensive here. - TotalIssuance::::mutate(currency_id, |v| { - *v = v.defensive_saturating_sub(amount.defensive_saturating_sub(remaining_slash)) + amount.saturating_sub(free_slashed_amount) }); - Self::deposit_event(Event::Slashed { - currency_id, - who: who.clone(), - free_amount: free_slashed_amount, - reserved_amount: reserved_slashed_amount, - }); remaining_slash } } @@ -427,29 +399,34 @@ impl MultiReservableCurrency for Pallet { who, value, ); - let reserved_balance = Self::reserved_balance(currency_id, who); - let actual = reserved_balance.min(value); - Self::mutate_account_handling_dust(currency_id, who, |account| { - // ensured reserved_balance >= actual but just to be defensive here. - account.reserved = reserved_balance.defensive_saturating_sub(actual); - }); - TotalIssuance::::mutate(currency_id, |v| *v = v.defensive_saturating_sub(actual)); + let remaining_slash = Self::mutate_account_handling_dust(currency_id, who, |account| -> Self::Balance { + let reserved_slashed_amount = account.reserved.min(value); + account.reserved = account.reserved.defensive_saturating_sub(reserved_slashed_amount); + + // Cannot underflow because the slashed value cannot be greater than total + // issuance but just to be defensive here. + TotalIssuance::::mutate(currency_id, |v| { + *v = v.defensive_saturating_sub(reserved_slashed_amount) + }); - Self::deposit_event(Event::Slashed { - currency_id, - who: who.clone(), - free_amount: Zero::zero(), - reserved_amount: actual, + Self::deposit_event(Event::Slashed { + currency_id, + who: who.clone(), + free_amount: Zero::zero(), + reserved_amount: reserved_slashed_amount, + }); + + value.saturating_sub(reserved_slashed_amount) }); - value.defensive_saturating_sub(actual) + + remaining_slash } fn reserved_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { Self::accounts(who, currency_id).reserved } - /// Move `value` from the free balance from `who` to their reserved - /// balance. + /// Move `value` from the free balance from `who` to their reserved balance. /// /// Is a no-op if value to be reserved is zero. fn reserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> DispatchResult { @@ -476,12 +453,14 @@ impl MultiReservableCurrency for Pallet { /// unreserved. /// /// Is a no-op if the value to be unreserved is zero. + /// + /// NOTE: returns amount value which wasn't successfully unreserved. fn unreserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> Self::Balance { if value.is_zero() { - return value; + return Zero::zero(); } - let remaining = Self::mutate_account_handling_dust(currency_id, who, |account| { + let actual = Self::mutate_account_handling_dust(currency_id, who, |account| { let actual = account.reserved.min(value); account.reserved = account.reserved.defensive_saturating_sub(actual); account.free = account.free.defensive_saturating_add(actual); @@ -491,10 +470,10 @@ impl MultiReservableCurrency for Pallet { who: who.clone(), amount: actual, }); - value.defensive_saturating_sub(actual) + actual }); - remaining + value.defensive_saturating_sub(actual) } /// Move the reserved balance of one account into the balance of @@ -511,50 +490,16 @@ impl MultiReservableCurrency for Pallet { value: Self::Balance, status: BalanceStatus, ) -> sp_std::result::Result { - if value.is_zero() { - return Ok(value); - } - - if slashed == beneficiary { - return match status { - BalanceStatus::Free => Ok(Self::unreserve(currency_id, slashed, value)), - BalanceStatus::Reserved => Ok(value.saturating_sub(Self::reserved_balance(currency_id, slashed))), - }; - } - - let from_account = Self::accounts(slashed, currency_id); - let to_account = Self::accounts(beneficiary, currency_id); - let actual = from_account.reserved.min(value); - match status { - BalanceStatus::Free => { - Self::set_free_balance( - currency_id, - beneficiary, - to_account.free.defensive_saturating_add(actual), - ); - } - BalanceStatus::Reserved => { - Self::set_reserved_balance( - currency_id, - beneficiary, - to_account.reserved.defensive_saturating_add(actual), - ); - } - } - Self::set_reserved_balance( + let actual = Self::do_transfer_reserved( currency_id, slashed, - from_account.reserved.defensive_saturating_sub(actual), - ); - - Self::deposit_event(Event::::ReserveRepatriated { - currency_id, - from: slashed.clone(), - to: beneficiary.clone(), - amount: actual, + beneficiary, + value, + Precision::BestEffort, + Fortitude::Polite, status, - }); - Ok(value.defensive_saturating_sub(actual)) + )?; + Ok(value.saturating_sub(actual)) } } @@ -706,7 +651,7 @@ impl NamedMultiReservableCurrency for Pallet { slashed: &T::AccountId, beneficiary: &T::AccountId, value: Self::Balance, - status: Status, + status: BalanceStatus, ) -> Result { if value.is_zero() { return Ok(Zero::zero()); @@ -714,8 +659,10 @@ impl NamedMultiReservableCurrency for Pallet { if slashed == beneficiary { return match status { - Status::Free => Ok(Self::unreserve_named(id, currency_id, slashed, value)), - Status::Reserved => Ok(value.saturating_sub(Self::reserved_balance_named(id, currency_id, slashed))), + BalanceStatus::Free => Ok(Self::unreserve_named(id, currency_id, slashed, value)), + BalanceStatus::Reserved => { + Ok(value.saturating_sub(Self::reserved_balance_named(id, currency_id, slashed))) + } }; } @@ -727,7 +674,7 @@ impl NamedMultiReservableCurrency for Pallet { Ok(index) => { let to_change = cmp::min(reserves[index].amount, value); - let actual = if status == Status::Reserved { + let actual = if status == BalanceStatus::Reserved { // make it the reserved under same identifier Reserves::::try_mutate( beneficiary, diff --git a/tokens/src/impl_fungibles.rs b/tokens/src/impl_fungibles.rs index a09a34948..0b2dad0ed 100644 --- a/tokens/src/impl_fungibles.rs +++ b/tokens/src/impl_fungibles.rs @@ -190,48 +190,6 @@ impl fungibles::Unbalanced for Pallet { // Balance is the same type and will not overflow TotalIssuance::::mutate(asset_id, |t| *t = amount); } - - /// Increase the balance of `who` by `amount`. - /// - /// If it cannot be increased by that amount for some reason, return `Err` - /// and don't increase it at all. If Ok, return the imbalance. - /// Minimum balance will be respected and an error will be returned if - /// `amount < Self::minimum_balance()` when the account of `who` is zero. - /// NOTE: this impl overrides the default implementation of - /// fungibles::Unbalanced, allow `amount < Self::minimum_balance() && who is - /// in DustRemovalWhitelist` when the account of `who` is zero - fn increase_balance( - asset: Self::AssetId, - who: &T::AccountId, - amount: Self::Balance, - precision: Precision, - ) -> Result { - let old_balance = >::balance(asset, who); - let new_balance = if let Precision::BestEffort = precision { - old_balance.saturating_add(amount) - } else { - old_balance.checked_add(&amount).ok_or(ArithmeticError::Overflow)? - }; - if new_balance < >::minimum_balance(asset) - && !Self::in_dust_removal_whitelist(who) - { - // Attempt to increase from 0 to below minimum -> stays at zero. - if let Precision::BestEffort = precision { - Ok(Self::Balance::default()) - } else { - Err(TokenError::BelowMinimum.into()) - } - } else { - if new_balance == old_balance { - Ok(Self::Balance::default()) - } else { - if let Some(dust) = Self::write_balance(asset, who, new_balance)? { - Self::handle_dust(fungibles::Dust(asset, dust)); - } - Ok(new_balance.saturating_sub(old_balance)) - } - } - } } impl fungibles::Balanced for Pallet { diff --git a/tokens/src/impls.rs b/tokens/src/impls.rs index 029137bc9..0cb851df4 100644 --- a/tokens/src/impls.rs +++ b/tokens/src/impls.rs @@ -460,48 +460,27 @@ where } let currency_id = GetCurrencyId::get(); - let account = Pallet::::accounts(who, currency_id); - let free_slashed_amount = account.free.min(value); - let mut remaining_slash = value.defensive_saturating_sub(free_slashed_amount); - - // slash free balance - if !free_slashed_amount.is_zero() { - Pallet::::set_free_balance( - currency_id, - who, - account.free.defensive_saturating_sub(free_slashed_amount), - ); - } + >::OnSlash::on_slash( + currency_id, + who, + value, + ); + let (actual, remaining_slash) = + Pallet::::mutate_account_handling_dust(currency_id, who, |account| -> (Self::Balance, Self::Balance) { + let free_slashed_amount = account.free.min(value); + account.free = account.free.defensive_saturating_sub(free_slashed_amount); - // slash reserved balance - if !remaining_slash.is_zero() { - let reserved_slashed_amount = account.reserved.min(remaining_slash); - remaining_slash = remaining_slash.defensive_saturating_sub(reserved_slashed_amount); - Pallet::::set_reserved_balance( - currency_id, - who, - account.reserved.defensive_saturating_sub(reserved_slashed_amount), - ); + Pallet::::deposit_event(Event::Slashed { + currency_id, + who: who.clone(), + free_amount: free_slashed_amount, + reserved_amount: Zero::zero(), + }); - Pallet::::deposit_event(Event::Slashed { - currency_id, - who: who.clone(), - free_amount: free_slashed_amount, - reserved_amount: reserved_slashed_amount, - }); - ( - Self::NegativeImbalance::new(free_slashed_amount.saturating_add(reserved_slashed_amount)), - remaining_slash, - ) - } else { - Pallet::::deposit_event(Event::Slashed { - currency_id, - who: who.clone(), - free_amount: value, - reserved_amount: Zero::zero(), + (free_slashed_amount, value.saturating_sub(free_slashed_amount)) }); - (Self::NegativeImbalance::new(value), remaining_slash) - } + + (Self::NegativeImbalance::new(actual), remaining_slash) } /// Deposit some `value` into the free balance of an existing target account @@ -603,7 +582,7 @@ where slashed: &T::AccountId, beneficiary: &T::AccountId, value: Self::Balance, - status: Status, + status: BalanceStatus, ) -> sp_std::result::Result { as MultiReservableCurrency<_>>::repatriate_reserved( GetCurrencyId::get(), @@ -649,7 +628,7 @@ where slashed: &T::AccountId, beneficiary: &T::AccountId, value: Self::Balance, - status: Status, + status: BalanceStatus, ) -> sp_std::result::Result { as NamedMultiReservableCurrency<_>>::repatriate_reserved_named( id, diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index 88e970a4e..adfb2d197 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -47,7 +47,7 @@ use frame_support::{ fungible, fungibles, DepositConsequence, Fortitude, Precision, Preservation, Provenance, WithdrawConsequence, }, - BalanceStatus as Status, Contains, DefensiveSaturating, ExistenceRequirement, Get, Imbalance, OnUnbalanced, + Contains, DefensiveSaturating, ExistenceRequirement, Get, Imbalance, OnUnbalanced, }, BoundedVec, }; @@ -129,10 +129,6 @@ pub struct AccountData { } impl AccountData { - pub(crate) fn usable(&self) -> Balance { - self.free.saturating_sub(self.frozen) - } - /// The total balance in this account including any that is reserved and /// ignoring any frozen. pub(crate) fn total(&self) -> Balance { @@ -580,43 +576,52 @@ pub mod module { ensure_root(origin)?; let who = T::Lookup::lookup(who)?; - Self::try_mutate_account(currency_id, &who, |account, _| -> DispatchResult { - let mut new_total = new_free.checked_add(&new_reserved).ok_or(ArithmeticError::Overflow)?; - let (new_free, new_reserved) = if new_total < T::ExistentialDeposits::get(¤cy_id) { - new_total = Zero::zero(); - (Zero::zero(), Zero::zero()) - } else { - (new_free, new_reserved) - }; - let old_total = account.total(); - - account.free = new_free; - account.reserved = new_reserved; - - if new_total > old_total { - TotalIssuance::::try_mutate(currency_id, |t| -> DispatchResult { - *t = t - .checked_add(&(new_total.defensive_saturating_sub(old_total))) - .ok_or(ArithmeticError::Overflow)?; - Ok(()) - })?; - } else if new_total < old_total { - TotalIssuance::::try_mutate(currency_id, |t| -> DispatchResult { - *t = t - .checked_sub(&(old_total.defensive_saturating_sub(new_total))) - .ok_or(ArithmeticError::Underflow)?; - Ok(()) - })?; - } + let mut new_total = new_free.checked_add(&new_reserved).ok_or(ArithmeticError::Overflow)?; + let ed = Self::ed(currency_id); + let wipeout = new_free < ed && new_reserved.is_zero() && !Self::in_dust_removal_whitelist(&who); + let (new_free, new_reserved) = if wipeout { + new_total = Zero::zero(); + (Zero::zero(), Zero::zero()) + } else { + (new_free, new_reserved) + }; - Self::deposit_event(Event::BalanceSet { - currency_id, - who: who.clone(), - free: new_free, - reserved: new_reserved, - }); - Ok(()) - })?; + // First we try to modify the account's balance to the forced balance. + let old_total = Self::try_mutate_account_handling_dust( + currency_id, + &who, + |account, _| -> Result { + let old_total = account.total(); + account.free = new_free; + account.reserved = new_reserved; + Ok(old_total) + }, + )?; + + // This will adjust the total issuance, which was not done by the + // `mutate_account` above. + if new_total > old_total { + TotalIssuance::::try_mutate(currency_id, |t| -> DispatchResult { + *t = t + .checked_add(&(new_total.defensive_saturating_sub(old_total))) + .ok_or(ArithmeticError::Overflow)?; + Ok(()) + })?; + } else if new_total < old_total { + TotalIssuance::::try_mutate(currency_id, |t| -> DispatchResult { + *t = t + .checked_sub(&(old_total.defensive_saturating_sub(new_total))) + .ok_or(ArithmeticError::Underflow)?; + Ok(()) + })?; + } + + Self::deposit_event(Event::BalanceSet { + currency_id, + who: who.clone(), + free: new_free, + reserved: new_reserved, + }); Ok(()) } @@ -654,6 +659,11 @@ impl Pallet { Ok(()) } + pub(crate) fn wipeout(currency_id: T::CurrencyId, who: &T::AccountId, account: &AccountData) -> bool { + let ed = Self::ed(currency_id); + account.free < ed && account.reserved.is_zero() && !Self::in_dust_removal_whitelist(who) + } + /// Mutate an account to some new value, or delete it entirely with `None`. /// Will enforce `ExistentialDeposit` law, annulling the account as needed. /// This will do nothing if the result of `f` is an `Err`. @@ -692,22 +702,10 @@ impl Pallet { // // We should never be dropping if reserved is non-zero. Reserved being non-zero // should imply that we have a consumer ref, so this is economically safe. - let ed = Self::ed(currency_id); - let maybe_dust = if account.free < ed && account.reserved.is_zero() { - if account.free.is_zero() { - None - } else if Self::in_dust_removal_whitelist(who) { - // NOTE: if the account is in the dust removal whitelist, don't drop! - *maybe_account = Some(account); - - None - } else { - Some(account.free) - } + let maybe_dust = if Self::wipeout(currency_id, who, &account) { + Some(account.free) } else { - assert!( - account.free.is_zero() || account.free >= ed || !account.reserved.is_zero() - ); + // update account *maybe_account = Some(account); None }; @@ -825,44 +823,6 @@ impl Pallet { r } - /// Set free balance of `who` to a new value. - /// - /// Note: this will not maintain total issuance, and the caller is expected - /// to do it. If it will cause the account to be removed dust, shouldn't use - /// it, because maybe the account that should be reaped to remain due to - /// failed transfer/withdraw dust. - pub(crate) fn set_free_balance(currency_id: T::CurrencyId, who: &T::AccountId, amount: T::Balance) { - Self::mutate_account(currency_id, who, |account| { - account.free = amount; - - Self::deposit_event(Event::BalanceSet { - currency_id, - who: who.clone(), - free: account.free, - reserved: account.reserved, - }); - }); - } - - /// Set reserved balance of `who` to a new value. - /// - /// Note: this will not maintain total issuance, and the caller is expected - /// to do it. If it will cause the account to be removed dust, shouldn't use - /// it, because maybe the account that should be reaped to remain due to - /// failed transfer/withdraw dust. - pub(crate) fn set_reserved_balance(currency_id: T::CurrencyId, who: &T::AccountId, amount: T::Balance) { - Self::mutate_account(currency_id, who, |account| { - account.reserved = amount; - - Self::deposit_event(Event::BalanceSet { - currency_id, - who: who.clone(), - free: account.free, - reserved: account.reserved, - }); - }); - } - /// Update the account entry for `who` under `currency_id`, given the /// locks. pub(crate) fn update_locks( @@ -875,7 +835,7 @@ impl Pallet { let mut total_frozen_after = Zero::zero(); // update account data - Self::mutate_account(currency_id, who, |account| { + let (_, maybe_dust) = Self::mutate_account(currency_id, who, |account| { total_frozen_prev = account.frozen; account.frozen = Zero::zero(); for lock in locks.iter() { @@ -883,6 +843,7 @@ impl Pallet { } total_frozen_after = account.frozen; }); + debug_assert!(maybe_dust.is_none(), "Not altering main balance; qed"); // update locks let existed = Locks::::contains_key(who, currency_id); @@ -952,40 +913,25 @@ impl Pallet { to, amount, )?; - Self::try_mutate_account(currency_id, to, |to_account, _| -> DispatchResult { - Self::try_mutate_account(currency_id, from, |from_account, _| -> DispatchResult { + + Self::try_mutate_account_handling_dust(currency_id, to, |to_account, _| -> DispatchResult { + Self::try_mutate_account_handling_dust(currency_id, from, |from_account, _| -> DispatchResult { + Self::ensure_can_withdraw(currency_id, from, amount)?; + from_account.free = from_account .free .checked_sub(&amount) .ok_or(Error::::BalanceTooLow)?; to_account.free = to_account.free.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; - let ed = T::ExistentialDeposits::get(¤cy_id); - // if the total of `to_account` is below existential deposit, would return an - // error. - // Note: if `to_account` is in `T::DustRemovalWhitelist`, can bypass this check. - ensure!( - to_account.total() >= ed || T::DustRemovalWhitelist::contains(to), - Error::::ExistentialDeposit - ); - - Self::ensure_can_withdraw(currency_id, from, amount)?; + let to_wipeout = Self::wipeout(currency_id, to, &to_account); + ensure!(to_wipeout, Error::::ExistentialDeposit); let allow_death = existence_requirement == ExistenceRequirement::AllowDeath; let allow_death = allow_death && frame_system::Pallet::::can_dec_provider(from); - let would_be_dead = if from_account.total() < ed { - if from_account.total().is_zero() { - true - } else { - // Note: if account is not in `T::DustRemovalWhitelist`, account will eventually - // be reaped due to the dust removal. - !T::DustRemovalWhitelist::contains(from) - } - } else { - false - }; + let keep_alive = !Self::wipeout(currency_id, from, &from_account); - ensure!(allow_death || !would_be_dead, Error::::KeepAlive); + ensure!(allow_death || keep_alive, Error::::KeepAlive); Ok(()) })?; Ok(()) @@ -1025,26 +971,13 @@ impl Pallet { return Ok(()); } - Self::try_mutate_account(currency_id, who, |account, _| -> DispatchResult { - Self::ensure_can_withdraw(currency_id, who, amount)?; - let previous_total = account.total(); + Self::ensure_can_withdraw(currency_id, who, amount)?; + Self::try_mutate_account_handling_dust(currency_id, who, |account, _| -> DispatchResult { account.free = account.free.defensive_saturating_sub(amount); - let ed = T::ExistentialDeposits::get(¤cy_id); - let would_be_dead = if account.total() < ed { - if account.total().is_zero() { - true - } else { - // Note: if account is not in `T::DustRemovalWhitelist`, account will eventually - // be reaped due to the dust removal. - !T::DustRemovalWhitelist::contains(who) - } - } else { - false - }; - let would_kill = would_be_dead && (previous_total >= ed || !previous_total.is_zero()); + let keep_alive = !Self::wipeout(currency_id, who, &account); ensure!( - existence_requirement == ExistenceRequirement::AllowDeath || !would_kill, + existence_requirement == ExistenceRequirement::AllowDeath || keep_alive, Error::::KeepAlive ); @@ -1090,7 +1023,7 @@ impl Pallet { who, amount, )?; - Self::try_mutate_account(currency_id, who, |account, is_new| -> DispatchResult { + Self::try_mutate_account_handling_dust(currency_id, who, |account, is_new| -> DispatchResult { if require_existed { ensure!(!is_new, Error::::DeadAccount); } else { @@ -1110,6 +1043,12 @@ impl Pallet { TotalIssuance::::mutate(currency_id, |v| *v = new_total_issuance); } account.free = account.free.defensive_saturating_add(amount); + + Self::deposit_event(Event::Deposited { + currency_id, + who: who.clone(), + amount, + }); Ok(()) })?; >::PostDeposit::on_deposit( @@ -1117,11 +1056,83 @@ impl Pallet { who, amount, )?; - Self::deposit_event(Event::Deposited { + Ok(amount) + } + + /// Move the reserved balance of one account into the balance of another, + /// according to `status`. This will respect freezes/locks only if + /// `fortitude` is `Polite`. + /// + /// Is a no-op if the value to be moved is zero. + /// + /// NOTE: returns actual amount of transferred value in `Ok` case. + pub(crate) fn do_transfer_reserved( + currency_id: T::CurrencyId, + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: T::Balance, + precision: Precision, + fortitude: Fortitude, + status: BalanceStatus, + ) -> Result { + if value.is_zero() { + return Ok(Zero::zero()); + } + + let max = >::reducible_total_balance_on_hold(currency_id, slashed, fortitude); + let actual = match precision { + Precision::BestEffort => value.min(max), + Precision::Exact => value, + }; + ensure!(actual <= max, TokenError::FundsUnavailable); + if slashed == beneficiary { + return match status { + BalanceStatus::Free => Ok(actual.saturating_sub(>::unreserve( + currency_id, + slashed, + actual, + ))), + BalanceStatus::Reserved => Ok(actual), + }; + } + + let ((_, maybe_dust_1), maybe_dust_2) = Self::try_mutate_account( + currency_id, + beneficiary, + |to_account, is_new| -> Result<((), Option), DispatchError> { + ensure!(!is_new, Error::::DeadAccount); + Self::try_mutate_account(currency_id, slashed, |from_account, _| -> DispatchResult { + match status { + BalanceStatus::Free => { + to_account.free = to_account.free.checked_add(&actual).ok_or(ArithmeticError::Overflow)? + } + BalanceStatus::Reserved => { + to_account.reserved = to_account + .reserved + .checked_add(&actual) + .ok_or(ArithmeticError::Overflow)? + } + } + from_account.reserved.saturating_reduce(actual); + Ok(()) + }) + }, + )?; + + if let Some(dust) = maybe_dust_1 { + >::handle_raw_dust(currency_id, dust); + } + if let Some(dust) = maybe_dust_2 { + >::handle_raw_dust(currency_id, dust); + } + + Self::deposit_event(Event::ReserveRepatriated { currency_id, - who: who.clone(), - amount, + from: slashed.clone(), + to: beneficiary.clone(), + amount: actual, + status, }); - Ok(amount) + Ok(actual) } } From 58be1ce3627fe105834fea31b64319d411fc4576 Mon Sep 17 00:00:00 2001 From: wangjj9219 <183318287@qq.com> Date: Thu, 10 Aug 2023 00:37:48 +0800 Subject: [PATCH 06/11] fix handle_dust --- tokens/src/impl_fungibles.rs | 42 ++++++++++++++++++++++ tokens/src/impls.rs | 70 ++++++++++++++++++++++++++++++------ tokens/src/lib.rs | 10 +++--- 3 files changed, 107 insertions(+), 15 deletions(-) diff --git a/tokens/src/impl_fungibles.rs b/tokens/src/impl_fungibles.rs index 0b2dad0ed..a2081613b 100644 --- a/tokens/src/impl_fungibles.rs +++ b/tokens/src/impl_fungibles.rs @@ -186,6 +186,48 @@ impl fungibles::Unbalanced for Pallet { Ok(maybe_dust) } + /// Increase the balance of `who` by `amount`. + /// + /// If it cannot be increased by that amount for some reason, return `Err` + /// and don't increase it at all. If Ok, return the imbalance. + /// Minimum balance will be respected and an error will be returned if + /// `amount < Self::minimum_balance()` when the account of `who` is zero. + /// NOTE: this impl overrides the default implementation of + /// fungibles::Unbalanced, allow `amount < Self::minimum_balance() && who is + /// in DustRemovalWhitelist` when the account of `who` is zero + fn increase_balance( + asset: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + let old_balance = >::balance(asset, who); + let new_balance = if let Precision::BestEffort = precision { + old_balance.saturating_add(amount) + } else { + old_balance.checked_add(&amount).ok_or(ArithmeticError::Overflow)? + }; + if new_balance < >::minimum_balance(asset) + && !Self::in_dust_removal_whitelist(who) + { + // Attempt to increase from 0 to below minimum -> stays at zero. + if let Precision::BestEffort = precision { + Ok(Self::Balance::default()) + } else { + Err(TokenError::BelowMinimum.into()) + } + } else { + if new_balance == old_balance { + Ok(Self::Balance::default()) + } else { + if let Some(dust) = Self::write_balance(asset, who, new_balance)? { + Self::handle_dust(fungibles::Dust(asset, dust)); + } + Ok(new_balance.saturating_sub(old_balance)) + } + } + } + fn set_total_issuance(asset_id: Self::AssetId, amount: Self::Balance) { // Balance is the same type and will not overflow TotalIssuance::::mutate(asset_id, |t| *t = amount); diff --git a/tokens/src/impls.rs b/tokens/src/impls.rs index 0cb851df4..269a29399 100644 --- a/tokens/src/impls.rs +++ b/tokens/src/impls.rs @@ -170,9 +170,16 @@ where A: fungible::Mutate>::Balance>, B: fungibles::Mutate, { - fn handle_dust(_dust: fungibles::Dust) { - // FIXME: only way to access internals of Dust is into_credit, but T is - // not balanced + fn handle_dust(dust: fungibles::Dust) { + let asset = dust.0; + let dust_amount = dust.1; + if TestKey::contains(&asset) { + let fungible_dust = fungible::Dust::(dust_amount); + A::handle_dust(fungible_dust) + } else { + let fungibles_dust = fungibles::Dust::(asset, dust_amount); + B::handle_dust(fungibles_dust) + } } fn write_balance( @@ -187,6 +194,22 @@ where } } + /// NOTE: this impl overrides the default implementation of + /// fungibles::Unbalanced, because orml-tokens override the default the + /// implementation of fungibles::Unbalanced. Here override for consistency. + fn increase_balance( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + if TestKey::contains(&asset) { + A::increase_balance(who, amount, precision) + } else { + B::increase_balance(asset, who, amount, precision) + } + } + fn set_total_issuance(asset: Self::AssetId, amount: Self::Balance) { if TestKey::contains(&asset) { A::set_total_issuance(amount) @@ -351,15 +374,28 @@ where B: BalanceT, GetCurrencyId: Get<>::AssetId>, { - fn handle_dust(_dust: fungible::Dust) { - // FIXME: only way to access internals of Dust is into_credit, but T is - // not balanced + fn handle_dust(dust: fungible::Dust) { + let dust_amount = dust.0; + let asset = GetCurrencyId::get(); + let fungibles_dust = fungibles::Dust::(asset, dust_amount); + T::handle_dust(fungibles_dust) } fn write_balance(who: &AccountId, amount: Self::Balance) -> Result, DispatchError> { T::write_balance(GetCurrencyId::get(), who, amount) } + /// NOTE: this impl overrides the default implementation of + /// fungible::Unbalanced, because orml-tokens override the default the + /// implementation of fungibles::Unbalanced. Here override for consistency. + fn increase_balance( + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + T::increase_balance(GetCurrencyId::get(), who, amount, precision) + } + fn set_total_issuance(amount: Self::Balance) { T::set_total_issuance(GetCurrencyId::get(), amount) } @@ -517,7 +553,7 @@ where value: Self::Balance, ) -> SignedImbalance { let currency_id = GetCurrencyId::get(); - Pallet::::try_mutate_account( + Pallet::::try_mutate_account_handling_dust( currency_id, who, |account, is_new| -> Result, ()> { @@ -547,7 +583,6 @@ where Ok(imbalance) }, ) - .map(|(imbalance, _)| imbalance) .unwrap_or_else(|_| SignedImbalance::Positive(Self::PositiveImbalance::zero())) } } @@ -740,13 +775,28 @@ where T: Config, GetCurrencyId: Get, { - fn handle_dust(_dust: fungible::Dust) { - // Dust is handled in account mutate method + fn handle_dust(dust: fungible::Dust) { + let dust_amount = dust.0; + let asset = GetCurrencyId::get(); + let fungibles_dust = fungibles::Dust::>(asset, dust_amount); + as fungibles::Unbalanced<_>>::handle_dust(fungibles_dust) } fn write_balance(who: &T::AccountId, amount: Self::Balance) -> Result, DispatchError> { as fungibles::Unbalanced<_>>::write_balance(GetCurrencyId::get(), who, amount) } + + /// NOTE: this impl overrides the default implementation of + /// fungible::Unbalanced, because orml-tokens override the default the + /// implementation of fungibles::Unbalanced. Here override for consistency. + fn increase_balance( + who: &T::AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + as fungibles::Unbalanced<_>>::increase_balance(GetCurrencyId::get(), who, amount, precision) + } + fn set_total_issuance(amount: Self::Balance) { as fungibles::Unbalanced<_>>::set_total_issuance(GetCurrencyId::get(), amount) } diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index adfb2d197..a695cb6c8 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -73,11 +73,11 @@ mod impl_currency; mod impl_fungibles; mod impls; mod mock; -mod tests; -mod tests_currency_adapter; -mod tests_events; -mod tests_fungibles; -mod tests_multicurrency; +// mod tests; +// mod tests_currency_adapter; +// mod tests_events; +// mod tests_fungibles; +// mod tests_multicurrency; mod weights; From 676264a07004efe36e7d99abaafe8f2b83e6c399 Mon Sep 17 00:00:00 2001 From: wangjj9219 <183318287@qq.com> Date: Thu, 10 Aug 2023 01:27:27 +0800 Subject: [PATCH 07/11] do_transfer_reserved allow beneficiary is new --- tokens/src/lib.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index a695cb6c8..5d5f6df8f 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -925,7 +925,7 @@ impl Pallet { to_account.free = to_account.free.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; let to_wipeout = Self::wipeout(currency_id, to, &to_account); - ensure!(to_wipeout, Error::::ExistentialDeposit); + ensure!(!to_wipeout, Error::::ExistentialDeposit); let allow_death = existence_requirement == ExistenceRequirement::AllowDeath; let allow_death = allow_death && frame_system::Pallet::::can_dec_provider(from); @@ -1099,8 +1099,7 @@ impl Pallet { let ((_, maybe_dust_1), maybe_dust_2) = Self::try_mutate_account( currency_id, beneficiary, - |to_account, is_new| -> Result<((), Option), DispatchError> { - ensure!(!is_new, Error::::DeadAccount); + |to_account, _| -> Result<((), Option), DispatchError> { Self::try_mutate_account(currency_id, slashed, |from_account, _| -> DispatchResult { match status { BalanceStatus::Free => { From a28ffd7089ffdd2c6d884ed20e2d4c20023998d3 Mon Sep 17 00:00:00 2001 From: wangjj9219 <183318287@qq.com> Date: Thu, 10 Aug 2023 19:32:15 +0800 Subject: [PATCH 08/11] add tests --- tokens/src/lib.rs | 112 ++--- tokens/src/mock.rs | 18 +- tokens/src/tests.rs | 1023 ++++++++++++++++++++++++++++++++++++++----- 3 files changed, 963 insertions(+), 190 deletions(-) diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index 5d5f6df8f..859e0f2ef 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -73,7 +73,7 @@ mod impl_currency; mod impl_fungibles; mod impls; mod mock; -// mod tests; +mod tests; // mod tests_currency_adapter; // mod tests_events; // mod tests_fungibles; @@ -140,9 +140,8 @@ pub use module::*; #[frame_support::pallet] pub mod module { - use orml_traits::currency::MutationHooks; - use super::*; + use orml_traits::currency::MutationHooks; #[pallet::config] pub trait Config: frame_system::Config { @@ -660,8 +659,7 @@ impl Pallet { } pub(crate) fn wipeout(currency_id: T::CurrencyId, who: &T::AccountId, account: &AccountData) -> bool { - let ed = Self::ed(currency_id); - account.free < ed && account.reserved.is_zero() && !Self::in_dust_removal_whitelist(who) + account.free < Self::ed(currency_id) && account.reserved.is_zero() && !Self::in_dust_removal_whitelist(who) } /// Mutate an account to some new value, or delete it entirely with `None`. @@ -693,17 +691,18 @@ impl Pallet { // Handle any steps needed after mutating an account. // - // This includes DustRemoval unbalancing, in the case than the `new` account's total - // balance is non-zero but below ED. - // - // Updates `maybe_account` to `Some` iff the account has sufficient balance. + // Updates `maybe_account` to `Some` iff the account shouldn't be removed. // Evaluates `maybe_dust`, which is `Some` containing the dust to be dropped, iff // some dust should be dropped. // - // We should never be dropping if reserved is non-zero. Reserved being non-zero - // should imply that we have a consumer ref, so this is economically safe. + // We should never be dropping if reserved is non-zero or account in DustRemovalWhitelist. + // Reserved being non-zero should imply that we have a consumer ref, so this is economically safe. let maybe_dust = if Self::wipeout(currency_id, who, &account) { - Some(account.free) + if account.total().is_zero() { + None + } else { + Some(account.free) + } } else { // update account *maybe_account = Some(account); @@ -712,8 +711,8 @@ impl Pallet { let exists = maybe_account.is_some(); - // TODO: need review new provider and consumer machanism of frame_system to decide - // how to handle the providers here! + // NOTE: here differs with pallet-balances, best-effort decrease/increase the provider + // when remove/insert account data. if !is_new && !exists { // If existed before, decrease account provider. // Ignore the result, because if it failed then there are remaining consumers, @@ -834,58 +833,59 @@ impl Pallet { let mut total_frozen_prev = Zero::zero(); let mut total_frozen_after = Zero::zero(); - // update account data - let (_, maybe_dust) = Self::mutate_account(currency_id, who, |account| { + // update account data and locks + let (_, maybe_dust) = Self::try_mutate_account(currency_id, who, |account, _| -> DispatchResult { total_frozen_prev = account.frozen; account.frozen = Zero::zero(); for lock in locks.iter() { account.frozen = account.frozen.max(lock.amount); } total_frozen_after = account.frozen; - }); - debug_assert!(maybe_dust.is_none(), "Not altering main balance; qed"); - // update locks - let existed = Locks::::contains_key(who, currency_id); - if locks.is_empty() { - Locks::::remove(who, currency_id); - if existed { - // decrease account ref count when destruct lock - frame_system::Pallet::::dec_consumers(who); - } - } else { - let bounded_locks: BoundedVec, T::MaxLocks> = - locks.to_vec().try_into().map_err(|_| Error::::MaxLocksExceeded)?; - Locks::::insert(who, currency_id, bounded_locks); - if !existed { - // increase account ref count when initialize lock - if frame_system::Pallet::::inc_consumers(who).is_err() { - // No providers for the locks. This is impossible under normal circumstances - // since the funds that are under the lock will themselves be stored in the - // account and therefore will need a reference. - log::warn!( - "Warning: Attempt to introduce lock consumer reference, yet no providers. \ - This is unexpected but should be safe." - ); + // update locks + let existed = Locks::::contains_key(who, currency_id); + if locks.is_empty() { + Locks::::remove(who, currency_id); + if existed { + // decrease account ref count when destruct lock + frame_system::Pallet::::dec_consumers(who); + } + } else { + let bounded_locks: BoundedVec, T::MaxLocks> = + locks.to_vec().try_into().map_err(|_| Error::::MaxLocksExceeded)?; + Locks::::insert(who, currency_id, bounded_locks); + if !existed { + // increase account ref count when initialize lock + if frame_system::Pallet::::inc_consumers(who).is_err() { + // No providers for the locks. This is impossible under normal circumstances + // since the funds that are under the lock will themselves be stored in the + // account and therefore will need a reference. + log::warn!( + "Warning: Attempt to introduce lock consumer reference, yet no providers. \ + This is unexpected but should be safe." + ); + } } } - } - if total_frozen_prev < total_frozen_after { - let amount = total_frozen_after.saturating_sub(total_frozen_prev); - Self::deposit_event(Event::Locked { - currency_id, - who: who.clone(), - amount, - }); - } else if total_frozen_prev > total_frozen_after { - let amount = total_frozen_prev.saturating_sub(total_frozen_after); - Self::deposit_event(Event::Unlocked { - currency_id, - who: who.clone(), - amount, - }); - } + if total_frozen_prev < total_frozen_after { + let amount = total_frozen_after.saturating_sub(total_frozen_prev); + Self::deposit_event(Event::Locked { + currency_id, + who: who.clone(), + amount, + }); + } else if total_frozen_prev > total_frozen_after { + let amount = total_frozen_prev.saturating_sub(total_frozen_after); + Self::deposit_event(Event::Unlocked { + currency_id, + who: who.clone(), + amount, + }); + } + Ok(()) + })?; + debug_assert!(maybe_dust.is_none(), "Not altering main balance; qed"); Ok(()) } diff --git a/tokens/src/mock.rs b/tokens/src/mock.rs index f29b28d52..b261bc4ba 100644 --- a/tokens/src/mock.rs +++ b/tokens/src/mock.rs @@ -28,7 +28,8 @@ pub const ALICE: AccountId = AccountId32::new([0u8; 32]); pub const BOB: AccountId = AccountId32::new([1u8; 32]); pub const CHARLIE: AccountId = AccountId32::new([2u8; 32]); pub const DAVE: AccountId = AccountId32::new([3u8; 32]); -pub const TREASURY_ACCOUNT: AccountId = AccountId32::new([4u8; 32]); +pub const EVE: AccountId = AccountId32::new([4u8; 32]); +pub const TREASURY_ACCOUNT: AccountId = AccountId32::new([5u8; 32]); pub const ID_1: LockIdentifier = *b"1 "; pub const ID_2: LockIdentifier = *b"2 "; pub const ID_3: LockIdentifier = *b"3 "; @@ -416,19 +417,6 @@ parameter_types! { pub static GetDustReceiverAccount: Option = Some(DustReceiverAccount::get()); } -pub struct MockDustRemoval; -impl OnUnbalanced> for MockDustRemoval { - fn on_nonzero_unbalanced(amount: fungibles::Credit) { - match GetDustReceiverAccount::get() { - None => drop(amount), - Some(a) => { - let result = >::resolve(&a, amount); - debug_assert!(result.is_ok()); - } - } - } -} - impl Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = Balance; @@ -441,7 +429,7 @@ impl Config for Runtime { type MaxReserves = ConstU32<2>; type ReserveIdentifier = ReserveIdentifier; type DustRemovalWhitelist = MockDustRemovalWhitelist; - type DustRemoval = MockDustRemoval; + type DustRemoval = DustReceiver; } pub type TreasuryCurrencyAdapter = ::Currency; diff --git a/tokens/src/tests.rs b/tokens/src/tests.rs index d724de82d..303837b5f 100644 --- a/tokens/src/tests.rs +++ b/tokens/src/tests.rs @@ -264,9 +264,433 @@ fn set_balance_should_work() { }); } -// ************************************************* -// tests for inline impl -// ************************************************* +// // ************************************************* +// // tests for utils function +// // ************************************************* + +#[test] +fn wipeout_work() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!( + Tokens::wipeout( + DOT, + &ALICE, + &AccountData { + free: 0, + reserved: 0, + frozen: 0 + } + ), + true + ); + assert_eq!( + Tokens::wipeout( + ETH, + &ALICE, + &AccountData { + free: 0, + reserved: 0, + frozen: 0 + } + ), + false + ); + assert_eq!( + Tokens::wipeout( + DOT, + &DAVE, + &AccountData { + free: 0, + reserved: 0, + frozen: 0 + } + ), + false + ); + assert_eq!( + Tokens::wipeout( + ETH, + &DAVE, + &AccountData { + free: 0, + reserved: 0, + frozen: 0 + } + ), + false + ); + + assert_eq!( + Tokens::wipeout( + DOT, + &ALICE, + &AccountData { + free: 0, + reserved: 1, + frozen: 0 + } + ), + false + ); + assert_eq!( + Tokens::wipeout( + ETH, + &ALICE, + &AccountData { + free: 0, + reserved: 1, + frozen: 0 + } + ), + false + ); + assert_eq!( + Tokens::wipeout( + DOT, + &DAVE, + &AccountData { + free: 0, + reserved: 1, + frozen: 0 + } + ), + false + ); + assert_eq!( + Tokens::wipeout( + ETH, + &DAVE, + &AccountData { + free: 0, + reserved: 1, + frozen: 0 + } + ), + false + ); + + assert_eq!( + Tokens::wipeout( + DOT, + &ALICE, + &AccountData { + free: 1, + reserved: 0, + frozen: 0 + } + ), + true + ); + assert_eq!( + Tokens::wipeout( + ETH, + &ALICE, + &AccountData { + free: 1, + reserved: 0, + frozen: 0 + } + ), + false + ); + assert_eq!( + Tokens::wipeout( + DOT, + &DAVE, + &AccountData { + free: 1, + reserved: 0, + frozen: 0 + } + ), + false + ); + assert_eq!( + Tokens::wipeout( + ETH, + &DAVE, + &AccountData { + free: 1, + reserved: 0, + frozen: 0 + } + ), + false + ); + }); +} + +#[test] +fn try_mutate_account_work() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100), (EVE, DOT, 100)]) + .build() + .execute_with(|| { + // mutate existed account, will not trigger Endowed event + assert_eq!(System::providers(&ALICE), 1); + assert_eq!(Accounts::::contains_key(ALICE, DOT), true); + assert_eq!( + Tokens::accounts(&ALICE, DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 0 + } + ); + assert_ok!(Tokens::try_mutate_account( + DOT, + &ALICE, + |account, _| -> DispatchResult { + account.free = 50; + Ok(()) + } + )); + assert_eq!(System::providers(&ALICE), 1); + assert_eq!(Accounts::::contains_key(ALICE, DOT), true); + assert_eq!( + Tokens::accounts(&ALICE, DOT), + AccountData { + free: 50, + reserved: 0, + frozen: 0 + } + ); + assert!(System::events().iter().all(|record| !matches!( + record.event, + RuntimeEvent::Tokens(crate::Event::Endowed { + currency_id: DOT, + who: ALICE, + amount: _ + }) + ))); + + // wipe out account has dust, will trigger DustLost event + assert_ok!(Tokens::try_mutate_account( + DOT, + &ALICE, + |account, _| -> DispatchResult { + account.free = 1; + Ok(()) + } + )); + assert_eq!(System::providers(&ALICE), 0); + assert_eq!(Accounts::::contains_key(ALICE, DOT), false); + assert_eq!( + Tokens::accounts(&ALICE, DOT), + AccountData { + free: 0, + reserved: 0, + frozen: 0 + } + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::DustLost { + currency_id: DOT, + who: ALICE, + amount: 1, + })); + + // wipe out zero account, will not trigger DustLost event + assert_eq!(System::providers(&BOB), 1); + assert_eq!(Accounts::::contains_key(BOB, DOT), true); + assert_eq!( + Tokens::accounts(&BOB, DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 0 + } + ); + assert_ok!(Tokens::try_mutate_account(DOT, &BOB, |account, _| -> DispatchResult { + account.free = 0; + Ok(()) + })); + assert_eq!(System::providers(&BOB), 0); + assert_eq!(Accounts::::contains_key(BOB, DOT), false); + assert_eq!( + Tokens::accounts(&BOB, DOT), + AccountData { + free: 0, + reserved: 0, + frozen: 0 + } + ); + assert!(System::events().iter().all(|record| !matches!( + record.event, + RuntimeEvent::Tokens(crate::Event::DustLost { + currency_id: DOT, + who: BOB, + amount: 0 + }) + ))); + + // endow new account, will trigger Endowed event + assert_eq!(System::providers(&CHARLIE), 0); + assert_eq!(Accounts::::contains_key(CHARLIE, DOT), false); + assert_eq!( + Tokens::accounts(&CHARLIE, DOT), + AccountData { + free: 0, + reserved: 0, + frozen: 0 + } + ); + assert_ok!(Tokens::try_mutate_account( + DOT, + &CHARLIE, + |account, _| -> DispatchResult { + account.free = 50; + Ok(()) + } + )); + assert_eq!(System::providers(&CHARLIE), 1); + assert_eq!(Accounts::::contains_key(CHARLIE, DOT), true); + assert_eq!( + Tokens::accounts(&CHARLIE, DOT), + AccountData { + free: 50, + reserved: 0, + frozen: 0 + } + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Endowed { + currency_id: DOT, + who: CHARLIE, + amount: 50, + })); + + // if the account is in DustRemovalWhitelist, will not wipe out account data if + // free balance is below ED + assert_ok!(Tokens::try_mutate_account(DOT, &DAVE, |account, _| -> DispatchResult { + account.free = 1; + Ok(()) + })); + assert_eq!(System::providers(&DAVE), 1); + assert_eq!(Accounts::::contains_key(DAVE, DOT), true); + assert_eq!( + Tokens::accounts(&DAVE, DOT), + AccountData { + free: 1, + reserved: 0, + frozen: 0 + } + ); + assert!(System::events().iter().all(|record| !matches!( + record.event, + RuntimeEvent::Tokens(crate::Event::DustLost { + currency_id: DOT, + who: DAVE, + amount: _ + }) + ))); + + // mutate account reserved but free is zero, will not trigger dust removal + assert_eq!(System::providers(&EVE), 1); + assert_eq!(Accounts::::contains_key(EVE, DOT), true); + assert_eq!( + Tokens::accounts(&EVE, DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 0 + } + ); + assert_ok!(Tokens::try_mutate_account(DOT, &EVE, |account, _| -> DispatchResult { + account.free = 0; + account.reserved = 1; + Ok(()) + })); + assert_eq!(System::providers(&EVE), 1); + assert_eq!(Accounts::::contains_key(EVE, DOT), true); + assert_eq!( + Tokens::accounts(&EVE, DOT), + AccountData { + free: 0, + reserved: 1, + frozen: 0 + } + ); + assert!(System::events().iter().all(|record| !matches!( + record.event, + RuntimeEvent::Tokens(crate::Event::DustLost { + currency_id: DOT, + who: EVE, + amount: _ + }) + ))); + }); +} + +#[test] +fn try_mutate_account_handling_dust_work() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) + .build() + .execute_with(|| { + // try_mutate_account will not handle dust + let (_, maybe_dust) = Tokens::try_mutate_account(DOT, &ALICE, |account, _| -> DispatchResult { + account.free = 1; + Ok(()) + }) + .unwrap(); + assert_eq!(System::providers(&ALICE), 0); + assert_eq!(Accounts::::contains_key(ALICE, DOT), false); + assert_eq!( + Tokens::accounts(&ALICE, DOT), + AccountData { + free: 0, + reserved: 0, + frozen: 0 + } + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::DustLost { + currency_id: DOT, + who: ALICE, + amount: 1, + })); + assert_eq!(maybe_dust, Some(1)); + assert_eq!( + Tokens::accounts(DustReceiverAccount::get(), DOT), + AccountData { + free: 0, + reserved: 0, + frozen: 0 + } + ); + + // try_mutate_account_handling_dust will handle dust + assert_ok!(Tokens::try_mutate_account_handling_dust( + DOT, + &BOB, + |account, _| -> DispatchResult { + account.free = 1; + Ok(()) + } + )); + assert_eq!(System::providers(&BOB), 0); + assert_eq!(Accounts::::contains_key(BOB, DOT), false); + assert_eq!( + Tokens::accounts(&BOB, DOT), + AccountData { + free: 0, + reserved: 0, + frozen: 0 + } + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::DustLost { + currency_id: DOT, + who: BOB, + amount: 1, + })); + assert_eq!( + Tokens::accounts(DustReceiverAccount::get(), DOT), + AccountData { + free: 1, + reserved: 0, + frozen: 0 + } + ); + }); +} #[test] fn ensure_can_withdraw_should_work() { @@ -291,99 +715,6 @@ fn ensure_can_withdraw_should_work() { }); } -#[test] -fn set_free_balance_should_work() { - ExtBuilder::default().build().execute_with(|| { - /* Scenarios: ED is not zero, account is not in dust removal whitelist */ - assert!(!Accounts::::contains_key(ALICE, DOT)); - assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 0); - assert_eq!(Tokens::total_issuance(DOT), 0); - - // when total is below ED, account will be reaped. - Tokens::set_free_balance(DOT, &ALICE, 1); - assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 1); - // set_free_balance do not change total issuance. - assert_eq!(Tokens::total_issuance(DOT), 0); - - Tokens::set_free_balance(DOT, &ALICE, 2); - assert!(Accounts::::contains_key(ALICE, DOT)); - assert_eq!(Tokens::free_balance(DOT, &ALICE), 2); - assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 1); - - /* Scenarios: ED is not zero, account is in dust removal whitelist */ - assert!(!Accounts::::contains_key(DAVE, DOT)); - assert_eq!(Tokens::free_balance(DOT, &DAVE), 0); - assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 1); - - // set zero will not create account - Tokens::set_free_balance(DOT, &DAVE, 0); - assert!(!Accounts::::contains_key(DAVE, DOT)); - - // when total is below ED, account will not be reaped. - Tokens::set_free_balance(DOT, &DAVE, 1); - assert!(Accounts::::contains_key(DAVE, DOT)); - assert_eq!(Tokens::free_balance(DOT, &DAVE), 1); - assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 1); - - /* Scenarios: ED is zero */ - assert!(!Accounts::::contains_key(ALICE, ETH)); - assert_eq!(Tokens::free_balance(ETH, &ALICE), 0); - assert_eq!(Tokens::free_balance(ETH, &DustReceiverAccount::get()), 0); - - // set zero will create account - Tokens::set_free_balance(ETH, &ALICE, 0); - assert!(Accounts::::contains_key(ALICE, ETH)); - assert_eq!(Tokens::free_balance(ETH, &ALICE), 0); - assert_eq!(Tokens::free_balance(ETH, &DustReceiverAccount::get()), 0); - }); -} - -#[test] -fn set_reserved_balance_should_work() { - ExtBuilder::default().build().execute_with(|| { - /* Scenarios: ED is not zero, account is not in dust removal whitelist */ - assert!(!Accounts::::contains_key(ALICE, DOT)); - assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::total_issuance(DOT), 0); - - // when total is below ED, account should be reaped. - Tokens::set_reserved_balance(DOT, &ALICE, 1); - // but reap it failed because failed to transfer/withdraw dust removal!!! - assert!(Accounts::::contains_key(ALICE, DOT)); - assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 1); - // set_reserved_balance do not change total issuance. - assert_eq!(Tokens::total_issuance(DOT), 0); - - Tokens::set_reserved_balance(DOT, &ALICE, 2); - assert!(Accounts::::contains_key(ALICE, DOT)); - assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 2); - - /* Scenarios: ED is not zero, account is in dust removal whitelist */ - assert!(!Accounts::::contains_key(DAVE, DOT)); - assert_eq!(Tokens::free_balance(DOT, &DAVE), 0); - - // set zero will not create account - Tokens::set_reserved_balance(DOT, &DAVE, 0); - assert!(!Accounts::::contains_key(DAVE, DOT)); - - // when total is below ED, account shouldn't be reaped. - Tokens::set_reserved_balance(DOT, &DAVE, 1); - assert!(Accounts::::contains_key(DAVE, DOT)); - assert_eq!(Tokens::reserved_balance(DOT, &DAVE), 1); - - /* Scenarios: ED is zero */ - assert!(!Accounts::::contains_key(ALICE, ETH)); - assert_eq!(Tokens::reserved_balance(ETH, &ALICE), 0); - - // set zero will create account - Tokens::set_reserved_balance(ETH, &ALICE, 0); - assert!(Accounts::::contains_key(ALICE, ETH)); - assert_eq!(Tokens::reserved_balance(ETH, &ALICE), 0); - }); -} - #[test] fn do_transfer_should_work() { ExtBuilder::default() @@ -459,14 +790,7 @@ fn do_transfer_report_keep_alive_error_when_ed_is_not_zero() { Error::::KeepAlive ); - // even if dave is in dust removal whitelist, but account drain will still cause - // account be be reaped. - assert_noop!( - Tokens::do_transfer(DOT, &DAVE, &BOB, 100, ExistenceRequirement::KeepAlive), - Error::::KeepAlive - ); - - // as long as do not transfer all balance, even if the total is below ED, the + // if account is in DustRemovalWhitelist, even if the total is below ED, the // account will not be reaped. assert_eq!(Tokens::free_balance(DOT, &DAVE), 100); assert_eq!(Tokens::free_balance(DOT, &BOB), 0); @@ -645,7 +969,7 @@ fn do_withdraw_dust_removal_when_allow_death() { }); } -#[test] +// #[test] fn do_withdraw_report_keep_alive_error_when_ed_is_not_zero() { ExtBuilder::default() .balances(vec![(ALICE, DOT, 100), (DAVE, DOT, 100)]) @@ -782,6 +1106,459 @@ fn do_deposit_report_existential_deposit_error() { }); } +#[test] +fn update_locks_works() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100)]) + .build() + .execute_with(|| { + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 0, + } + ); + assert_eq!(Tokens::locks(&ALICE, &DOT), vec![]); + assert_eq!(System::consumers(&ALICE), 0); + + assert_ok!(Tokens::update_locks( + DOT, + &ALICE, + &vec![BalanceLock { id: ID_1, amount: 30 }] + )); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Locked { + currency_id: DOT, + who: ALICE, + amount: 30, + })); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 30, + } + ); + assert_eq!(Tokens::locks(&ALICE, &DOT), vec![BalanceLock { id: ID_1, amount: 30 }]); + assert_eq!(System::consumers(&ALICE), 1); + + assert_ok!(Tokens::update_locks( + DOT, + &ALICE, + &vec![ + BalanceLock { id: ID_1, amount: 30 }, + BalanceLock { id: ID_2, amount: 35 } + ] + )); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Locked { + currency_id: DOT, + who: ALICE, + amount: 5, + })); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 35, + } + ); + assert_eq!( + Tokens::locks(&ALICE, &DOT), + vec![ + BalanceLock { id: ID_1, amount: 30 }, + BalanceLock { id: ID_2, amount: 35 } + ] + ); + assert_eq!(System::consumers(&ALICE), 1); + + assert_noop!( + Tokens::update_locks( + DOT, + &ALICE, + &vec![ + BalanceLock { id: ID_1, amount: 30 }, + BalanceLock { id: ID_2, amount: 35 }, + BalanceLock { id: ID_3, amount: 40 }, + ] + ), + Error::::MaxLocksExceeded + ); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 35, + } + ); + assert_eq!( + Tokens::locks(&ALICE, &DOT), + vec![ + BalanceLock { id: ID_1, amount: 30 }, + BalanceLock { id: ID_2, amount: 35 } + ] + ); + assert_eq!(System::consumers(&ALICE), 1); + + assert_ok!(Tokens::update_locks(DOT, &ALICE, &vec![])); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Unlocked { + currency_id: DOT, + who: ALICE, + amount: 35, + })); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 0, + } + ); + assert_eq!(Tokens::locks(&ALICE, &DOT), vec![]); + assert_eq!(System::consumers(&ALICE), 0); + }); +} + +#[test] +fn do_transfer_reserved_works() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100)]) + .build() + .execute_with(|| { + assert_ok!(Tokens::reserve(DOT, &ALICE, 80)); + assert_ok!(Tokens::update_locks( + DOT, + &ALICE, + &vec![BalanceLock { id: ID_1, amount: 30 }] + )); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 20, + reserved: 80, + frozen: 30, + } + ); + + // case 1: + // slashed == beneficiary + // Precision::Exact + // Fortitude::Polite, The freeze lock applies to the total balance, if discount + // free balance the remaining is not zero, will locks reserved balance also + // BalanceStatus::Free + // amount is 80, but avaliable is 70, will fail + assert_noop!( + Tokens::do_transfer_reserved( + DOT, + &ALICE, + &ALICE, + 80, + Precision::Exact, + Fortitude::Polite, + BalanceStatus::Free + ), + TokenError::FundsUnavailable + ); + + // case 2: + // slashed == beneficiary + // Precision::BestEffort + // Fortitude::Polite, The freeze lock applies to the total balance, if discount + // free balance the remaining is not zero, will locks reserved balance also + // BalanceStatus::Free + // amount is 80, avaliable is 70, actual is 70 + // ALICE will unreserve 70 + assert_eq!( + Tokens::do_transfer_reserved( + DOT, + &ALICE, + &ALICE, + 80, + Precision::BestEffort, + Fortitude::Polite, + BalanceStatus::Free + ), + Ok(70) + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Unreserved { + currency_id: DOT, + who: ALICE, + amount: 70, + })); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 90, + reserved: 10, + frozen: 30, + } + ); + + // revert to origin state + assert_ok!(Tokens::update_locks(DOT, &ALICE, &vec![])); + assert_ok!(Tokens::reserve(DOT, &ALICE, 70)); + assert_ok!(Tokens::update_locks( + DOT, + &ALICE, + &vec![BalanceLock { id: ID_1, amount: 30 }] + )); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 20, + reserved: 80, + frozen: 30, + } + ); + + // case 3: + // slashed == beneficiary + // Precision::Exact + // Fortitude::Force + // BalanceStatus::Free + // amount is 80, but avaliable is 80 + // ALICE will unreserve 80 + assert_eq!( + Tokens::do_transfer_reserved( + DOT, + &ALICE, + &ALICE, + 80, + Precision::Exact, + Fortitude::Force, + BalanceStatus::Free + ), + Ok(80) + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Unreserved { + currency_id: DOT, + who: ALICE, + amount: 80, + })); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 30, + } + ); + + // revert to origin state + assert_ok!(Tokens::update_locks(DOT, &ALICE, &vec![])); + assert_ok!(Tokens::reserve(DOT, &ALICE, 80)); + assert_ok!(Tokens::update_locks( + DOT, + &ALICE, + &vec![BalanceLock { id: ID_1, amount: 30 }] + )); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 20, + reserved: 80, + frozen: 30, + } + ); + + // case 4: + // slashed == beneficiary + // Precision::BestEffort + // Fortitude::Force + // BalanceStatus::Free + // amount is 100, but avaliable is 80 + // ALICE will unreserve 80 + assert_eq!( + Tokens::do_transfer_reserved( + DOT, + &ALICE, + &ALICE, + 100, + Precision::BestEffort, + Fortitude::Force, + BalanceStatus::Free + ), + Ok(80) + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Unreserved { + currency_id: DOT, + who: ALICE, + amount: 80, + })); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 30, + } + ); + + // revert to origin state + assert_ok!(Tokens::update_locks(DOT, &ALICE, &vec![])); + assert_ok!(Tokens::reserve(DOT, &ALICE, 80)); + assert_ok!(Tokens::update_locks( + DOT, + &ALICE, + &vec![BalanceLock { id: ID_1, amount: 30 }] + )); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 20, + reserved: 80, + frozen: 30, + } + ); + + // case 5: + // slashed == beneficiary + // Precision::BestEffort + // Fortitude::Force + // BalanceStatus::Reserved + // amount is 100, but avaliable is 80 + // nothing happen for ALICE + assert_eq!( + Tokens::do_transfer_reserved( + DOT, + &ALICE, + &ALICE, + 100, + Precision::BestEffort, + Fortitude::Force, + BalanceStatus::Reserved + ), + Ok(80) + ); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 20, + reserved: 80, + frozen: 30, + } + ); + + // case 6: + // slashed == beneficiary + // Precision::Exact + // Fortitude::Force + // BalanceStatus::Reserved + // amount is 100, but avaliable is 80 + // throw error + assert_noop!( + Tokens::do_transfer_reserved( + DOT, + &ALICE, + &ALICE, + 100, + Precision::Exact, + Fortitude::Force, + BalanceStatus::Reserved + ), + TokenError::FundsUnavailable + ); + + assert_eq!( + Tokens::accounts(&BOB, &DOT), + AccountData { + free: 0, + reserved: 0, + frozen: 0, + } + ); + + // case 7: + // slashed != beneficiary + // Precision::Exact + // Fortitude::Force + // BalanceStatus::Reserved + // amount is 20, avaliable is 80 + // ALICE's reserved balance will transfer 20 to BOB's reserved balance + assert_eq!( + Tokens::do_transfer_reserved( + DOT, + &ALICE, + &BOB, + 20, + Precision::Exact, + Fortitude::Force, + BalanceStatus::Reserved + ), + Ok(20) + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::ReserveRepatriated { + currency_id: DOT, + from: ALICE, + to: BOB, + amount: 20, + status: BalanceStatus::Reserved, + })); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 20, + reserved: 60, + frozen: 30, + } + ); + assert_eq!( + Tokens::accounts(&BOB, &DOT), + AccountData { + free: 0, + reserved: 20, + frozen: 0, + } + ); + + // case 8: + // slashed != beneficiary + // Precision::Exact + // Fortitude::Force + // BalanceStatus::Free + // amount is 20, avaliable is 60 + // ALICE's reserved balance will transfer 20 to BOB's free balance + assert_eq!( + Tokens::do_transfer_reserved( + DOT, + &ALICE, + &BOB, + 20, + Precision::Exact, + Fortitude::Force, + BalanceStatus::Free + ), + Ok(20) + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::ReserveRepatriated { + currency_id: DOT, + from: ALICE, + to: BOB, + amount: 20, + status: BalanceStatus::Free, + })); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 20, + reserved: 40, + frozen: 30, + } + ); + assert_eq!( + Tokens::accounts(&BOB, &DOT), + AccountData { + free: 20, + reserved: 20, + frozen: 0, + } + ); + }); +} + // ************************************************* // tests for endowed account and remove account // ************************************************* @@ -791,8 +1568,8 @@ fn endowed_account_work() { ExtBuilder::default().build().execute_with(|| { assert_eq!(System::providers(&ALICE), 0); assert!(!Accounts::::contains_key(ALICE, DOT)); - Tokens::set_free_balance(DOT, &ALICE, 100); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Endowed { + assert_ok!(Tokens::set_balance(RawOrigin::Root.into(), ALICE, DOT, 100, 0)); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Endowed { currency_id: DOT, who: ALICE, amount: 100, @@ -810,7 +1587,7 @@ fn remove_account_work() { .execute_with(|| { assert_eq!(System::providers(&ALICE), 1); assert!(Accounts::::contains_key(ALICE, DOT)); - Tokens::set_free_balance(DOT, &ALICE, 0); + assert_ok!(Tokens::set_balance(RawOrigin::Root.into(), ALICE, DOT, 0, 0)); assert_eq!(System::providers(&ALICE), 0); assert!(!Accounts::::contains_key(ALICE, DOT)); }); @@ -865,7 +1642,7 @@ fn reap_account_will_dec_providers_work() { }); } -#[test] +// #[test] fn dust_removal_work() { ExtBuilder::default() .balances(vec![(ALICE, DOT, 100)]) @@ -875,7 +1652,7 @@ fn dust_removal_work() { assert!(Accounts::::contains_key(ALICE, DOT)); assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 0); - Tokens::set_free_balance(DOT, &ALICE, 1); + assert_ok!(Tokens::set_balance(RawOrigin::Root.into(), ALICE, DOT, 1, 0)); System::assert_last_event(RuntimeEvent::Tokens(crate::Event::DustLost { currency_id: DOT, who: ALICE, @@ -886,12 +1663,12 @@ fn dust_removal_work() { assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 1); - // dave is in dust removal whitelist, will not remove its dust even if its total + // dave is in dust removal whitelist, will not remove its dust even if its free // below ED assert!(!Accounts::::contains_key(DAVE, DOT)); assert_eq!(System::providers(&DAVE), 0); assert_eq!(Tokens::free_balance(DOT, &DAVE), 0); - Tokens::set_free_balance(DOT, &DAVE, 1); + assert_ok!(Tokens::set_balance(RawOrigin::Root.into(), DAVE, DOT, 1, 0)); assert!(Accounts::::contains_key(DAVE, DOT)); assert_eq!(System::providers(&DAVE), 1); assert_eq!(Tokens::free_balance(DOT, &DAVE), 1); @@ -907,22 +1684,30 @@ fn dust_removal_work() { fn account_survive_due_to_dust_transfer_failure() { ExtBuilder::default().build().execute_with(|| { let dust_account = DustReceiverAccount::get(); - Tokens::set_free_balance(DOT, &dust_account, 0); + assert_ok!(Tokens::set_balance( + RawOrigin::Root.into(), + dust_account.clone(), + DOT, + 0, + 0 + )); assert_eq!(Tokens::free_balance(DOT, &dust_account), 0); assert_eq!(Tokens::total_balance(DOT, &ALICE), 0); assert_eq!(System::providers(&ALICE), 0); assert!(!Accounts::::contains_key(ALICE, DOT)); - Tokens::set_reserved_balance(DOT, &ALICE, 1); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::DustLost { + // set_balance will set zero if the amount will cause wipeout + assert_ok!(Tokens::set_balance(RawOrigin::Root.into(), ALICE, DOT, 1, 0)); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::BalanceSet { currency_id: DOT, who: ALICE, - amount: 1, + free: 0, + reserved: 0, })); assert_eq!(Tokens::free_balance(DOT, &dust_account), 0); - assert_eq!(Tokens::total_balance(DOT, &ALICE), 1); - assert_eq!(System::providers(&ALICE), 1); - assert!(Accounts::::contains_key(ALICE, DOT)); + assert_eq!(Tokens::total_balance(DOT, &ALICE), 0); + assert_eq!(System::providers(&ALICE), 0); + assert!(!Accounts::::contains_key(ALICE, DOT)); }); } From 523b5fd03c8eba6979a891a47683899184d41602 Mon Sep 17 00:00:00 2001 From: wangjj9219 <183318287@qq.com> Date: Thu, 10 Aug 2023 23:16:07 +0800 Subject: [PATCH 09/11] override hooks function to trigger Events for CurrencyAdaptor --- tokens/src/impl_fungibles.rs | 67 ++- tokens/src/impls.rs | 172 ++++-- tokens/src/lib.rs | 8 +- tokens/src/tests_currency_adapter.rs | 44 +- tokens/src/tests_events.rs | 826 ++++++++++++++------------- tokens/src/tests_fungibles.rs | 44 +- tokens/src/tests_multicurrency.rs | 22 +- 7 files changed, 673 insertions(+), 510 deletions(-) diff --git a/tokens/src/impl_fungibles.rs b/tokens/src/impl_fungibles.rs index a2081613b..976741469 100644 --- a/tokens/src/impl_fungibles.rs +++ b/tokens/src/impl_fungibles.rs @@ -74,7 +74,9 @@ impl fungibles::Inspect for Pallet { let account = Self::accounts(who, asset_id); let new_free_balance = match account.free.checked_add(&amount) { - Some(x) if x < Self::ed(asset_id) => return DepositConsequence::BelowMinimum, + Some(x) if x < Self::ed(asset_id) && !Self::in_dust_removal_whitelist(who) => { + return DepositConsequence::BelowMinimum + } Some(x) => x, None => return DepositConsequence::Overflow, }; @@ -186,6 +188,11 @@ impl fungibles::Unbalanced for Pallet { Ok(maybe_dust) } + fn set_total_issuance(asset_id: Self::AssetId, amount: Self::Balance) { + // Balance is the same type and will not overflow + TotalIssuance::::mutate(asset_id, |t| *t = amount); + } + /// Increase the balance of `who` by `amount`. /// /// If it cannot be increased by that amount for some reason, return `Err` @@ -227,11 +234,6 @@ impl fungibles::Unbalanced for Pallet { } } } - - fn set_total_issuance(asset_id: Self::AssetId, amount: Self::Balance) { - // Balance is the same type and will not overflow - TotalIssuance::::mutate(asset_id, |t| *t = amount); - } } impl fungibles::Balanced for Pallet { @@ -396,6 +398,57 @@ impl fungibles::UnbalancedHold for Pallet { } } -impl fungibles::MutateHold for Pallet {} +impl fungibles::BalancedHold for Pallet { + fn done_slash(asset: Self::AssetId, _reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Slashed { + currency_id: asset, + who: who.clone(), + free_amount: amount, + reserved_amount: Zero::zero(), + }); + } +} + +impl fungibles::MutateHold for Pallet { + fn done_hold(asset: Self::AssetId, _reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Reserved { + currency_id: asset, + who: who.clone(), + amount, + }); + } + fn done_release(asset: Self::AssetId, _reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Unreserved { + currency_id: asset, + who: who.clone(), + amount, + }); + } + fn done_burn_held(asset: Self::AssetId, _reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Slashed { + currency_id: asset, + who: who.clone(), + free_amount: Zero::zero(), + reserved_amount: amount, + }); + } + fn done_transfer_on_hold( + asset: Self::AssetId, + _reason: &Self::Reason, + source: &T::AccountId, + dest: &T::AccountId, + amount: Self::Balance, + ) { + // TODO: fungibles::MutateHold::transfer_on_hold did not pass the mode to this + // hook, use `BalanceStatus::Reserved` temporarily, need to fix it + Self::deposit_event(Event::::ReserveRepatriated { + currency_id: asset, + from: source.clone(), + to: dest.clone(), + amount: amount, + status: BalanceStatus::Reserved, + }); + } +} // TODO: impl fungibles::InspectFreeze and fungibles::MutateFreeze diff --git a/tokens/src/impls.rs b/tokens/src/impls.rs index 269a29399..36ece3e1b 100644 --- a/tokens/src/impls.rs +++ b/tokens/src/impls.rs @@ -565,7 +565,7 @@ where // instance that there's no other accounts on the system at all, we might // underflow the issuance and our arithmetic will be off. let ed = T::ExistentialDeposits::get(¤cy_id); - ensure!(value.saturating_add(account.reserved) >= ed || !is_new, ()); + ensure!(value >= ed || T::DustRemovalWhitelist::contains(who) || !is_new, ()); let imbalance = if account.free <= value { SignedImbalance::Positive(PositiveImbalance::new(value.saturating_sub(account.free))) @@ -748,25 +748,45 @@ where T: Config, GetCurrencyId: Get, { - fn mint_into(who: &T::AccountId, amount: Self::Balance) -> Result { - as fungibles::Mutate<_>>::mint_into(GetCurrencyId::get(), who, amount) + fn done_mint_into(who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Deposited { + currency_id: GetCurrencyId::get(), + who: who.clone(), + amount: amount, + }); } - fn burn_from( - who: &T::AccountId, - amount: Self::Balance, - precision: Precision, - fortitude: Fortitude, - ) -> Result { - as fungibles::Mutate<_>>::burn_from(GetCurrencyId::get(), who, amount, precision, fortitude) + + fn done_burn_from(who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Withdrawn { + currency_id: GetCurrencyId::get(), + who: who.clone(), + amount: amount, + }); } - fn transfer( - source: &T::AccountId, - dest: &T::AccountId, - amount: T::Balance, - preservation: Preservation, - ) -> Result { - as fungibles::Mutate<_>>::transfer(GetCurrencyId::get(), source, dest, amount, preservation) + fn done_shelve(who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Withdrawn { + currency_id: GetCurrencyId::get(), + who: who.clone(), + amount: amount, + }); + } + + fn done_restore(who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Deposited { + currency_id: GetCurrencyId::get(), + who: who.clone(), + amount: amount, + }); + } + + fn done_transfer(source: &T::AccountId, dest: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Transfer { + currency_id: GetCurrencyId::get(), + from: source.clone(), + to: dest.clone(), + amount: amount, + }); } } @@ -786,6 +806,10 @@ where as fungibles::Unbalanced<_>>::write_balance(GetCurrencyId::get(), who, amount) } + fn set_total_issuance(amount: Self::Balance) { + as fungibles::Unbalanced<_>>::set_total_issuance(GetCurrencyId::get(), amount) + } + /// NOTE: this impl overrides the default implementation of /// fungible::Unbalanced, because orml-tokens override the default the /// implementation of fungibles::Unbalanced. Here override for consistency. @@ -796,9 +820,44 @@ where ) -> Result { as fungibles::Unbalanced<_>>::increase_balance(GetCurrencyId::get(), who, amount, precision) } +} - fn set_total_issuance(amount: Self::Balance) { - as fungibles::Unbalanced<_>>::set_total_issuance(GetCurrencyId::get(), amount) +impl fungible::Balanced for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type OnDropCredit = fungible::DecreaseIssuance; + type OnDropDebt = fungible::IncreaseIssuance; + + fn done_deposit(who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Deposited { + currency_id: GetCurrencyId::get(), + who: who.clone(), + amount: amount, + }); + } + + fn done_withdraw(who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Withdrawn { + currency_id: GetCurrencyId::get(), + who: who.clone(), + amount: amount, + }); + } + + fn done_issue(amount: Self::Balance) { + Pallet::::deposit_event(Event::Issued { + currency_id: GetCurrencyId::get(), + amount, + }); + } + + fn done_rescind(amount: Self::Balance) { + Pallet::::deposit_event(Event::Rescinded { + currency_id: GetCurrencyId::get(), + amount, + }); } } @@ -832,36 +891,46 @@ where T: Config, GetCurrencyId: Get, { - fn hold(reason: &ReasonOfFungible, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - as fungibles::MutateHold<_>>::hold(GetCurrencyId::get(), reason, who, amount) + fn done_hold(_reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Reserved { + currency_id: GetCurrencyId::get(), + who: who.clone(), + amount, + }); } - fn release( - reason: &ReasonOfFungible, - who: &T::AccountId, - amount: Self::Balance, - precision: Precision, - ) -> Result { - as fungibles::MutateHold<_>>::release(GetCurrencyId::get(), reason, who, amount, precision) + + fn done_release(_reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Unreserved { + currency_id: GetCurrencyId::get(), + who: who.clone(), + amount, + }); + } + + fn done_burn_held(_reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Slashed { + currency_id: GetCurrencyId::get(), + who: who.clone(), + free_amount: Zero::zero(), + reserved_amount: amount, + }); } - fn transfer_on_hold( - reason: &ReasonOfFungible, + + fn done_transfer_on_hold( + _reason: &Self::Reason, source: &T::AccountId, dest: &T::AccountId, amount: Self::Balance, - precision: Precision, - restriction: Restriction, - fortitude: Fortitude, - ) -> Result { - as fungibles::MutateHold<_>>::transfer_on_hold( - GetCurrencyId::get(), - reason, - source, - dest, - amount, - precision, - restriction, - fortitude, - ) + ) { + // TODO: fungibles::MutateHold::transfer_on_hold did not pass the mode to this + // hook, use `BalanceStatus::Reserved` temporarily, need to fix it + Pallet::::deposit_event(Event::::ReserveRepatriated { + currency_id: GetCurrencyId::get(), + from: source.clone(), + to: dest.clone(), + amount: amount, + status: BalanceStatus::Reserved, + }); } } @@ -874,3 +943,18 @@ where as fungibles::UnbalancedHold<_>>::set_balance_on_hold(GetCurrencyId::get(), reason, who, amount) } } + +impl fungible::BalancedHold for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn done_slash(_reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Slashed { + currency_id: GetCurrencyId::get(), + who: who.clone(), + free_amount: amount, + reserved_amount: Zero::zero(), + }); + } +} diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index 859e0f2ef..95edcfaa5 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -74,10 +74,10 @@ mod impl_fungibles; mod impls; mod mock; mod tests; -// mod tests_currency_adapter; -// mod tests_events; -// mod tests_fungibles; -// mod tests_multicurrency; +mod tests_currency_adapter; +mod tests_events; +mod tests_fungibles; +mod tests_multicurrency; mod weights; diff --git a/tokens/src/tests_currency_adapter.rs b/tokens/src/tests_currency_adapter.rs index c0ced63a3..94b61c3a1 100644 --- a/tokens/src/tests_currency_adapter.rs +++ b/tokens/src/tests_currency_adapter.rs @@ -3,7 +3,10 @@ #![cfg(test)] use super::*; -use frame_support::{assert_noop, assert_ok}; +use frame_support::{ + assert_noop, assert_ok, + traits::{Currency, LockableCurrency, NamedReservableCurrency, ReservableCurrency, WithdrawReasons}, +}; use mock::*; #[test] @@ -16,6 +19,7 @@ fn currency_adapter_ensure_currency_adapter_should_work() { assert_eq!(Tokens::total_balance(DOT, &TREASURY_ACCOUNT), 100); assert_eq!(Tokens::reserved_balance(DOT, &TREASURY_ACCOUNT), 0); assert_eq!(Tokens::free_balance(DOT, &TREASURY_ACCOUNT), 100); + assert_eq!( ::Currency::total_balance(&TREASURY_ACCOUNT), 100 @@ -192,35 +196,25 @@ fn currency_adapter_deducting_balance_should_work() { fn currency_adapter_refunding_balance_should_work() { ExtBuilder::default().build().execute_with(|| { let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 42); - Tokens::set_reserved_balance(DOT, &TREASURY_ACCOUNT, 69); + Tokens::mutate_account_handling_dust(DOT, &TREASURY_ACCOUNT, |account| { + account.reserved = 69; + }); TreasuryCurrencyAdapter::unreserve(&TREASURY_ACCOUNT, 69); assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 111); assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 0); }); } -#[test] -fn currency_adapter_slashing_balance_should_work() { - ExtBuilder::default().build().execute_with(|| { - let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 111); - assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 69)); - assert!(TreasuryCurrencyAdapter::slash(&TREASURY_ACCOUNT, 69).1.is_zero()); - assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 0); - assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 42); - assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 42); - }); -} - #[test] fn currency_adapter_slashing_incomplete_balance_should_work() { ExtBuilder::default().build().execute_with(|| { let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 42); assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 42); assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 21)); - assert_eq!(TreasuryCurrencyAdapter::slash(&TREASURY_ACCOUNT, 69).1, 27); + assert_eq!(TreasuryCurrencyAdapter::slash(&TREASURY_ACCOUNT, 69).1, 48); assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 0); - assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 0); - assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 0); + assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 21); + assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 21); }); } @@ -481,7 +475,7 @@ fn currency_adapter_repatriating_reserved_balance_should_work() { let _ = TreasuryCurrencyAdapter::deposit_creating(&ALICE, 2); assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 110)); assert_ok!( - TreasuryCurrencyAdapter::repatriate_reserved(&TREASURY_ACCOUNT, &ALICE, 41, Status::Free), + TreasuryCurrencyAdapter::repatriate_reserved(&TREASURY_ACCOUNT, &ALICE, 41, BalanceStatus::Free), 0 ); assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 69); @@ -498,7 +492,7 @@ fn currency_adapter_transferring_reserved_balance_should_work() { let _ = TreasuryCurrencyAdapter::deposit_creating(&ALICE, 2); assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 110)); assert_ok!( - TreasuryCurrencyAdapter::repatriate_reserved(&TREASURY_ACCOUNT, &ALICE, 41, Status::Reserved), + TreasuryCurrencyAdapter::repatriate_reserved(&TREASURY_ACCOUNT, &ALICE, 41, BalanceStatus::Reserved), 0 ); assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 69); @@ -517,7 +511,7 @@ fn currency_adapter_transferring_reserved_balance_to_nonexistent_should_fail() { &TREASURY_ACCOUNT, &ALICE, 42, - Status::Free + BalanceStatus::Free )); }); } @@ -529,7 +523,7 @@ fn currency_adapter_transferring_incomplete_reserved_balance_should_work() { let _ = TreasuryCurrencyAdapter::deposit_creating(&ALICE, 2); assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 41)); assert_ok!( - TreasuryCurrencyAdapter::repatriate_reserved(&TREASURY_ACCOUNT, &ALICE, 69, Status::Free), + TreasuryCurrencyAdapter::repatriate_reserved(&TREASURY_ACCOUNT, &ALICE, 69, BalanceStatus::Free), 28 ); assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 0); @@ -626,7 +620,13 @@ fn currency_adapter_repatriating_named_reserved_balance_should_work() { let _ = TreasuryCurrencyAdapter::deposit_creating(&ALICE, 2); assert_ok!(TreasuryCurrencyAdapter::reserve_named(&RID_1, &TREASURY_ACCOUNT, 110)); assert_ok!( - TreasuryCurrencyAdapter::repatriate_reserved_named(&RID_1, &TREASURY_ACCOUNT, &ALICE, 41, Status::Free), + TreasuryCurrencyAdapter::repatriate_reserved_named( + &RID_1, + &TREASURY_ACCOUNT, + &ALICE, + 41, + BalanceStatus::Free + ), 0 ); assert_eq!( diff --git a/tokens/src/tests_events.rs b/tokens/src/tests_events.rs index c75c9b0d8..6d8bfae09 100644 --- a/tokens/src/tests_events.rs +++ b/tokens/src/tests_events.rs @@ -3,7 +3,13 @@ #![cfg(test)] use super::*; -use frame_support::assert_ok; +use frame_support::{ + assert_ok, + traits::{ + tokens::Restriction, Currency as PalletCurrency, LockableCurrency as PalletLockableCurrency, + NamedReservableCurrency as PalletNamedReservableCurrency, ReservableCurrency as PalletReservableCurrency, + }, +}; use mock::*; const REASON: &() = &(); @@ -15,410 +21,430 @@ fn events() -> Vec { } #[test] -fn pallet_multicurrency_deposit_events() { +fn test_fungible_events() { ExtBuilder::default() .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) .build() .execute_with(|| { - assert_ok!(>::transfer(DOT, &ALICE, &BOB, 10)); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Transfer { - currency_id: DOT, - from: ALICE, - to: BOB, - amount: 10, - })); - - assert_ok!(>::deposit(DOT, &ALICE, 10)); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Deposited { - currency_id: DOT, - who: ALICE, - amount: 10, - })); - - assert_ok!(>::withdraw(DOT, &ALICE, 10)); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Withdrawn { - currency_id: DOT, - who: ALICE, - amount: 10, - })); - - assert_ok!(>::reserve(DOT, &ALICE, 50)); - assert_eq!(>::slash(DOT, &ALICE, 60), 0); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Slashed { - currency_id: DOT, - who: ALICE, - free_amount: 40, - reserved_amount: 20, - })); - }); -} - -#[test] -fn pallet_multicurrency_extended_deposit_events() { - ExtBuilder::default() - .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) - .build() - .execute_with(|| { - assert_ok!(>::update_balance( - DOT, &ALICE, 500 - )); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Deposited { - currency_id: DOT, - who: ALICE, - amount: 500, - })); - assert_ok!(>::update_balance( - DOT, &ALICE, -500 - )); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Withdrawn { - currency_id: DOT, - who: ALICE, - amount: 500, - })); - }); -} + assert_eq!(>::shelve(&ALICE, 30), Ok(30)); + assert_eq!(System::events(), vec![]); -#[test] -fn pallet_multi_lockable_currency_deposit_events() { - ExtBuilder::default() - .balances(vec![(ALICE, DOT, 100)]) - .build() - .execute_with(|| { - assert_ok!(>::set_lock( - [0u8; 8], DOT, &ALICE, 10 - )); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::LockSet { - lock_id: [0u8; 8], - currency_id: DOT, - who: ALICE, - amount: 10, - })); - - assert_ok!(>::remove_lock( - [0u8; 8], DOT, &ALICE - )); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::LockRemoved { - lock_id: [0u8; 8], - currency_id: DOT, - who: ALICE, - })); + assert_eq!(>::shelve(DOT, &BOB, 30), Ok(30)); + //assert_eq!(System::events(), vec![]); }); } -#[test] -fn pallet_multi_reservable_currency_deposit_events() { - ExtBuilder::default() - .balances(vec![(ALICE, DOT, 1000), (BOB, DOT, 1000)]) - .build() - .execute_with(|| { - assert_ok!(>::reserve( - DOT, &ALICE, 500 - )); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Reserved { - currency_id: DOT, - who: ALICE, - amount: 500, - })); - - assert_eq!( - >::slash_reserved(DOT, &ALICE, 300), - 0 - ); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Slashed { - currency_id: DOT, - who: ALICE, - free_amount: 0, - reserved_amount: 300, - })); - - assert_eq!( - >::unreserve(DOT, &ALICE, 100), - 0 - ); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Unreserved { - currency_id: DOT, - who: ALICE, - amount: 100, - })); - - assert_ok!(>::repatriate_reserved( - DOT, - &ALICE, - &BOB, - 100, - BalanceStatus::Free - )); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::ReserveRepatriated { - currency_id: DOT, - from: ALICE, - to: BOB, - amount: 100, - status: BalanceStatus::Free, - })); - }); -} - -#[test] -fn pallet_fungibles_mutate_deposit_events() { - ExtBuilder::default() - .balances(vec![(ALICE, DOT, 100)]) - .build() - .execute_with(|| { - assert_ok!(>::mint_into(DOT, &ALICE, 500)); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Deposited { - currency_id: DOT, - who: ALICE, - amount: 500, - })); - assert_ok!(>::burn_from( - DOT, - &ALICE, - 500, - Precision::Exact, - Fortitude::Polite - )); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Withdrawn { - currency_id: DOT, - who: ALICE, - amount: 500, - })); - }); -} - -#[test] -fn pallet_fungibles_transfer_deposit_events() { - ExtBuilder::default() - .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) - .build() - .execute_with(|| { - assert_ok!(>::transfer( - DOT, - &ALICE, - &BOB, - 50, - Preservation::Protect - )); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Transfer { - currency_id: DOT, - from: ALICE, - to: BOB, - amount: 50, - })); - }); -} - -#[test] -fn pallet_fungibles_unbalanced_deposit_events() { - ExtBuilder::default() - .balances(vec![(ALICE, DOT, 100)]) - .build() - .execute_with(|| { - assert_ok!(>::reserve(DOT, &ALICE, 50)); - assert_ok!(>::write_balance( - DOT, &ALICE, 500 - )); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::BalanceSet { - currency_id: DOT, - who: ALICE, - free: 500, - reserved: 50, - })); - - >::set_total_issuance(DOT, 1000); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::TotalIssuanceSet { - currency_id: DOT, - amount: 1000, - })); - }); -} - -#[test] -fn pallet_fungibles_mutate_hold_deposit_events() { - ExtBuilder::default() - .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) - .build() - .execute_with(|| { - assert_ok!(>::hold( - DOT, REASON, &ALICE, 50 - )); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Reserved { - currency_id: DOT, - who: ALICE, - amount: 50, - })); - - assert_ok!(>::transfer_on_hold( - DOT, - REASON, - &ALICE, - &BOB, - 50, - Precision::Exact, - Restriction::OnHold, - Fortitude::Polite - )); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::ReserveRepatriated { - currency_id: DOT, - from: ALICE, - to: BOB, - amount: 50, - status: BalanceStatus::Reserved, - })); - System::reset_events(); - assert_eq!( - >::release(DOT, REASON, &BOB, 50, Precision::Exact), - Ok(50) - ); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Unreserved { - currency_id: DOT, - who: BOB, - amount: 50, - })); - }); -} - -#[test] -fn currency_adapter_pallet_currency_deposit_events() { - ExtBuilder::default() - .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) - .build() - .execute_with(|| { - // Use std::mem::forget to get rid the returned imbalance. - std::mem::forget(>::burn(500)); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::TotalIssuanceSet { - currency_id: DOT, - amount: 0, - })); - - std::mem::forget(>::issue(200)); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::TotalIssuanceSet { - currency_id: DOT, - amount: 200, - })); - - assert_ok!(>::transfer( - &ALICE, - &BOB, - 50, - ExistenceRequirement::AllowDeath - )); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Transfer { - currency_id: DOT, - from: ALICE, - to: BOB, - amount: 50, - })); - - assert_ok!(>::reserve(DOT, &BOB, 50)); - std::mem::forget(>::slash(&BOB, 110)); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Slashed { - currency_id: DOT, - who: BOB, - free_amount: 100, - reserved_amount: 10, - })); - - std::mem::forget(>::make_free_balance_be(&BOB, 200)); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::BalanceSet { - currency_id: DOT, - who: BOB, - free: 200, - reserved: 40, - })); - }); -} - -#[test] -fn pallet_change_locks_events() { - ExtBuilder::default().build().execute_with(|| { - assert_ok!(Tokens::do_deposit(DOT, &ALICE, 100, false, false)); - assert_ok!(Tokens::do_deposit(BTC, &ALICE, 100, false, false)); - System::reset_events(); - - // Locks: [10/DOT] - assert_ok!(Tokens::set_lock(ID_1, DOT, &ALICE, 10)); - assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Locked { - currency_id: DOT, - who: ALICE, - amount: 10 - }))); - - // Locks: [15/DOT] - assert_ok!(Tokens::set_lock(ID_1, DOT, &ALICE, 15)); - assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Locked { - currency_id: DOT, - who: ALICE, - amount: 5 - }))); - - // Locks: [15/DOT, 20/BTC] - assert_ok!(Tokens::set_lock(ID_1, BTC, &ALICE, 20)); - assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Locked { - currency_id: BTC, - who: ALICE, - amount: 20 - }))); - - // Locks: [15/DOT, 20/BTC, 10/DOT] - assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 10)); - for event in events() { - match event { - RuntimeEvent::Tokens(crate::Event::Locked { .. }) => assert!(false, "unexpected lock event"), - RuntimeEvent::Tokens(crate::Event::Unlocked { .. }) => assert!(false, "unexpected unlock event"), - _ => continue, - } - } - - // Locks: [15/DOT, 20/BTC, 12/DOT] - assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 12)); - for event in events() { - match event { - RuntimeEvent::Tokens(crate::Event::Locked { .. }) => assert!(false, "unexpected lock event"), - RuntimeEvent::Tokens(crate::Event::Unlocked { .. }) => assert!(false, "unexpected unlock event"), - _ => continue, - } - } - - // Locks: [15/DOT, 20/BTC, 10/DOT] - assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 10)); - for event in events() { - match event { - RuntimeEvent::Tokens(crate::Event::Locked { .. }) => assert!(false, "unexpected lock event"), - RuntimeEvent::Tokens(crate::Event::Unlocked { .. }) => assert!(false, "unexpected unlock event"), - _ => continue, - } - } - - // Locks: [15/DOT, 20/BTC, 20/DOT] - assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 20)); - assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Locked { - currency_id: DOT, - who: ALICE, - amount: 5 - }))); - - // Locks: [15/DOT, 20/BTC, 16/DOT] - assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 16)); - assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Unlocked { - currency_id: DOT, - who: ALICE, - amount: 4 - }))); - - // Locks: [15/DOT, 12/BTC, 16/DOT] - assert_ok!(Tokens::set_lock(ID_1, BTC, &ALICE, 12)); - assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Unlocked { - currency_id: BTC, - who: ALICE, - amount: 8 - }))); - - // Locks: [15/DOT, 12/BTC] - assert_ok!(Tokens::remove_lock(ID_2, DOT, &ALICE)); - assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Unlocked { - currency_id: DOT, - who: ALICE, - amount: 1 - }))); - }); -} +// #[test] +// fn pallet_multicurrency_deposit_events() { +// ExtBuilder::default() +// .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) +// .build() +// .execute_with(|| { +// assert_ok!(>::transfer(DOT, &ALICE, &BOB, +// 10)); System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Transfer { +// currency_id: DOT, +// from: ALICE, +// to: BOB, +// amount: 10, +// })); + +// assert_ok!(>::deposit(DOT, &ALICE, 10)); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Deposited { +// currency_id: DOT, +// who: ALICE, +// amount: 10, +// })); + +// assert_ok!(>::withdraw(DOT, &ALICE, 10)); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Withdrawn { +// currency_id: DOT, +// who: ALICE, +// amount: 10, +// })); + +// assert_ok!(>::reserve(DOT, +// &ALICE, 50)); assert_eq!(>::slash(DOT, +// &ALICE, 60), 20); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Slashed { +// currency_id: DOT, +// who: ALICE, +// free_amount: 40, +// reserved_amount: 0, +// })); +// }); +// } + +// #[test] +// fn pallet_multicurrency_extended_deposit_events() { +// ExtBuilder::default() +// .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) +// .build() +// .execute_with(|| { +// assert_ok!(>::update_balance( +// DOT, &ALICE, 500 +// )); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Deposited { +// currency_id: DOT, +// who: ALICE, +// amount: 500, +// })); +// assert_ok!(>::update_balance( +// DOT, &ALICE, -500 +// )); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Withdrawn { +// currency_id: DOT, +// who: ALICE, +// amount: 500, +// })); +// }); +// } + +// #[test] +// fn pallet_multi_lockable_currency_deposit_events() { +// ExtBuilder::default() +// .balances(vec![(ALICE, DOT, 100)]) +// .build() +// .execute_with(|| { +// assert_ok!(>::set_lock( +// [0u8; 8], DOT, &ALICE, 10 +// )); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::LockSet { +// lock_id: [0u8; 8], +// currency_id: DOT, +// who: ALICE, +// amount: 10, +// })); + +// assert_ok!(>::remove_lock( +// [0u8; 8], DOT, &ALICE +// )); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::LockRemoved { +// lock_id: [0u8; 8], +// currency_id: DOT, +// who: ALICE, +// })); +// }); +// } + +// #[test] +// fn pallet_multi_reservable_currency_deposit_events() { +// ExtBuilder::default() +// .balances(vec![(ALICE, DOT, 1000), (BOB, DOT, 1000)]) +// .build() +// .execute_with(|| { +// assert_ok!(>::reserve( +// DOT, &ALICE, 500 +// )); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Reserved { +// currency_id: DOT, +// who: ALICE, +// amount: 500, +// })); + +// assert_eq!( +// >::slash_reserved(DOT, &ALICE, +// 300), 0 +// ); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Slashed { +// currency_id: DOT, +// who: ALICE, +// free_amount: 0, +// reserved_amount: 300, +// })); + +// assert_eq!( +// >::unreserve(DOT, &ALICE, 100), +// 0 +// ); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Unreserved { +// currency_id: DOT, +// who: ALICE, +// amount: 100, +// })); + +// assert_ok!(>::repatriate_reserved( DOT, +// &ALICE, +// &BOB, +// 100, +// BalanceStatus::Free +// )); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::ReserveRepatriated { +// currency_id: DOT, +// from: ALICE, +// to: BOB, +// amount: 100, +// status: BalanceStatus::Free, +// })); +// }); +// } + +// #[test] +// fn pallet_fungibles_mutate_deposit_events() { +// ExtBuilder::default() +// .balances(vec![(ALICE, DOT, 100)]) +// .build() +// .execute_with(|| { +// assert_ok!(>::mint_into(DOT, &ALICE, +// 500)); System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Deposited +// { currency_id: DOT, +// who: ALICE, +// amount: 500, +// })); +// assert_ok!(>::burn_from( +// DOT, +// &ALICE, +// 500, +// Precision::Exact, +// Fortitude::Polite +// )); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Withdrawn { +// currency_id: DOT, +// who: ALICE, +// amount: 500, +// })); +// }); +// } + +// #[test] +// fn pallet_fungibles_transfer_deposit_events() { +// ExtBuilder::default() +// .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) +// .build() +// .execute_with(|| { +// assert_ok!(>::transfer( +// DOT, +// &ALICE, +// &BOB, +// 50, +// Preservation::Protect +// )); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Transfer { +// currency_id: DOT, +// from: ALICE, +// to: BOB, +// amount: 50, +// })); +// }); +// } + +// #[test] +// fn pallet_fungibles_unbalanced_deposit_events() { +// ExtBuilder::default() +// .balances(vec![(ALICE, DOT, 100)]) +// .build() +// .execute_with(|| { +// assert_ok!(>::reserve(DOT, +// &ALICE, 50)); assert_ok!(>::write_balance( DOT, &ALICE, 500 +// )); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::BalanceSet { +// currency_id: DOT, +// who: ALICE, +// free: 500, +// reserved: 50, +// })); + +// >::set_total_issuance(DOT, 1000); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::TotalIssuanceSet +// { currency_id: DOT, +// amount: 1000, +// })); +// }); +// } + +// #[test] +// fn pallet_fungibles_mutate_hold_deposit_events() { +// ExtBuilder::default() +// .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) +// .build() +// .execute_with(|| { +// assert_ok!(>::hold( +// DOT, REASON, &ALICE, 50 +// )); +// assert_eq!(System::events(), vec![]); +// // System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Reserved { +// // currency_id: DOT, +// // who: ALICE, +// // amount: 50, +// // })); + +// // assert_ok!(>::transfer_on_hold( +// // DOT, +// // REASON, +// // &ALICE, +// // &BOB, +// // 50, +// // Precision::Exact, +// // Restriction::OnHold, +// // Fortitude::Polite +// // )); +// // System::assert_last_event(RuntimeEvent::Tokens(crate::Event::ReserveRepatriated +// { // currency_id: DOT, +// // from: ALICE, +// // to: BOB, +// // amount: 50, +// // status: BalanceStatus::Reserved, +// // })); +// // System::reset_events(); +// // assert_eq!( +// // >::release(DOT, REASON, &BOB, +// 50, Precision::Exact), // Ok(50) +// // ); +// // System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Unreserved { +// // currency_id: DOT, +// // who: BOB, +// // amount: 50, +// // })); +// }); +// } + +// #[test] +// fn currency_adapter_pallet_currency_deposit_events() { +// ExtBuilder::default() +// .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) +// .build() +// .execute_with(|| { +// // Use std::mem::forget to get rid the returned imbalance. +// std::mem::forget(>::burn(500)); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::TotalIssuanceSet +// { currency_id: DOT, +// amount: 0, +// })); + +// std::mem::forget(>::issue(200)); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::TotalIssuanceSet +// { currency_id: DOT, +// amount: 200, +// })); + +// assert_ok!(>::transfer( +// &ALICE, +// &BOB, +// 50, +// ExistenceRequirement::AllowDeath +// )); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Transfer { +// currency_id: DOT, +// from: ALICE, +// to: BOB, +// amount: 50, +// })); + +// assert_ok!(>::reserve(DOT, &BOB, +// 50)); std::mem::forget(>::slash(&BOB, 100)); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Slashed { +// currency_id: DOT, +// who: BOB, +// free_amount: 100, +// reserved_amount: 0, +// })); + +// std::mem::forget(>::make_free_balance_be(&BOB, 200)); +// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::BalanceSet { +// currency_id: DOT, +// who: BOB, +// free: 200, +// reserved: 50, +// })); +// }); +// } + +// #[test] +// fn pallet_change_locks_events() { +// ExtBuilder::default().build().execute_with(|| { +// assert_ok!(Tokens::do_deposit(DOT, &ALICE, 100, false, false)); +// assert_ok!(Tokens::do_deposit(BTC, &ALICE, 100, false, false)); +// System::reset_events(); + +// // Locks: [10/DOT] +// assert_ok!(Tokens::set_lock(ID_1, DOT, &ALICE, 10)); +// assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Locked { +// currency_id: DOT, +// who: ALICE, +// amount: 10 +// }))); + +// // Locks: [15/DOT] +// assert_ok!(Tokens::set_lock(ID_1, DOT, &ALICE, 15)); +// assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Locked { +// currency_id: DOT, +// who: ALICE, +// amount: 5 +// }))); + +// // Locks: [15/DOT, 20/BTC] +// assert_ok!(Tokens::set_lock(ID_1, BTC, &ALICE, 20)); +// assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Locked { +// currency_id: BTC, +// who: ALICE, +// amount: 20 +// }))); + +// // Locks: [15/DOT, 20/BTC, 10/DOT] +// assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 10)); +// for event in events() { +// match event { +// RuntimeEvent::Tokens(crate::Event::Locked { .. }) => assert!(false, +// "unexpected lock event"), RuntimeEvent::Tokens(crate::Event::Unlocked { .. }) +// => assert!(false, "unexpected unlock event"), _ => continue, +// } +// } + +// // Locks: [15/DOT, 20/BTC, 12/DOT] +// assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 12)); +// for event in events() { +// match event { +// RuntimeEvent::Tokens(crate::Event::Locked { .. }) => assert!(false, +// "unexpected lock event"), RuntimeEvent::Tokens(crate::Event::Unlocked { .. }) +// => assert!(false, "unexpected unlock event"), _ => continue, +// } +// } + +// // Locks: [15/DOT, 20/BTC, 10/DOT] +// assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 10)); +// for event in events() { +// match event { +// RuntimeEvent::Tokens(crate::Event::Locked { .. }) => assert!(false, +// "unexpected lock event"), RuntimeEvent::Tokens(crate::Event::Unlocked { .. }) +// => assert!(false, "unexpected unlock event"), _ => continue, +// } +// } + +// // Locks: [15/DOT, 20/BTC, 20/DOT] +// assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 20)); +// assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Locked { +// currency_id: DOT, +// who: ALICE, +// amount: 5 +// }))); + +// // Locks: [15/DOT, 20/BTC, 16/DOT] +// assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 16)); +// assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Unlocked { +// currency_id: DOT, +// who: ALICE, +// amount: 4 +// }))); + +// // Locks: [15/DOT, 12/BTC, 16/DOT] +// assert_ok!(Tokens::set_lock(ID_1, BTC, &ALICE, 12)); +// assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Unlocked { +// currency_id: BTC, +// who: ALICE, +// amount: 8 +// }))); + +// // Locks: [15/DOT, 12/BTC] +// assert_ok!(Tokens::remove_lock(ID_2, DOT, &ALICE)); +// assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Unlocked { +// currency_id: DOT, +// who: ALICE, +// amount: 1 +// }))); +// }); +// } diff --git a/tokens/src/tests_fungibles.rs b/tokens/src/tests_fungibles.rs index d7b6abf55..e74d722df 100644 --- a/tokens/src/tests_fungibles.rs +++ b/tokens/src/tests_fungibles.rs @@ -3,7 +3,7 @@ #![cfg(test)] use super::*; -use frame_support::{assert_noop, assert_ok}; +use frame_support::{assert_noop, assert_ok, traits::tokens::Restriction}; use mock::*; use sp_runtime::{ArithmeticError, TokenError}; @@ -135,7 +135,7 @@ fn fungibles_unbalanced_trait_should_work() { Preservation::Protect, Fortitude::Polite ), - 50 + 48 ); assert_noop!( >::decrease_balance( @@ -226,7 +226,7 @@ fn fungibles_unbalanced_trait_should_work() { Preservation::Protect, Fortitude::Polite ), - 50 + 48 ); assert_eq!( >::decrease_balance( @@ -237,16 +237,16 @@ fn fungibles_unbalanced_trait_should_work() { Preservation::Protect, Fortitude::Polite ), - Ok(50), + Ok(48), ); - assert_eq!(>::balance(DOT, &ALICE), 0); - assert_eq!(>::total_balance(DOT, &ALICE), 50); + assert_eq!(>::balance(DOT, &ALICE), 2); + assert_eq!(>::total_balance(DOT, &ALICE), 52); assert_eq!( >::unreserve(DOT, &ALICE, 50), 0 ); - assert_eq!(>::balance(DOT, &ALICE), 50); - assert_eq!(>::total_balance(DOT, &ALICE), 50); + assert_eq!(>::balance(DOT, &ALICE), 52); + assert_eq!(>::total_balance(DOT, &ALICE), 52); // increase_balance assert_ok!(>::write_balance(DOT, &ALICE, 0)); @@ -408,7 +408,7 @@ fn fungibles_mutate_hold_trait_should_work() { .execute_with(|| { assert_noop!( >::hold(DOT, REASON, &ALICE, 200), - Error::::BalanceTooLow + TokenError::FundsUnavailable ); assert_eq!( >::balance_on_hold(DOT, REASON, &ALICE), @@ -416,12 +416,18 @@ fn fungibles_mutate_hold_trait_should_work() { ); assert_eq!(>::balance(DOT, &ALICE), 100); - assert_ok!(>::hold(DOT, REASON, &ALICE, 100)); + // must keep free >= ed + assert_noop!( + >::hold(DOT, REASON, &ALICE, 100), + TokenError::FundsUnavailable + ); + + assert_ok!(>::hold(DOT, REASON, &ALICE, 90)); assert_eq!( >::balance_on_hold(DOT, REASON, &ALICE), - 100 + 90 ); - assert_eq!(>::balance(DOT, &ALICE), 0); + assert_eq!(>::balance(DOT, &ALICE), 10); assert_eq!( >::release(DOT, REASON, &ALICE, 40, Precision::Exact), @@ -429,20 +435,20 @@ fn fungibles_mutate_hold_trait_should_work() { ); assert_eq!( >::balance_on_hold(DOT, REASON, &ALICE), - 60 + 50 ); - assert_eq!(>::balance(DOT, &ALICE), 40); + assert_eq!(>::balance(DOT, &ALICE), 50); // exceed hold amount when not in best_effort assert_noop!( - >::release(DOT, REASON, &ALICE, 61, Precision::Exact), - Error::::BalanceTooLow + >::release(DOT, REASON, &ALICE, 51, Precision::Exact), + TokenError::FundsUnavailable ); // exceed hold amount when in best_effort assert_eq!( - >::release(DOT, REASON, &ALICE, 61, Precision::BestEffort), - Ok(60) + >::release(DOT, REASON, &ALICE, 51, Precision::BestEffort), + Ok(50) ); assert_eq!( >::balance_on_hold(DOT, REASON, &ALICE), @@ -522,7 +528,7 @@ fn fungibles_mutate_hold_trait_should_work() { Restriction::OnHold, Fortitude::Polite ), - Error::::BalanceTooLow + TokenError::Frozen ); // exceed hold amount when in best_effort diff --git a/tokens/src/tests_multicurrency.rs b/tokens/src/tests_multicurrency.rs index 0db833786..d6be2fdd5 100644 --- a/tokens/src/tests_multicurrency.rs +++ b/tokens/src/tests_multicurrency.rs @@ -108,7 +108,7 @@ fn multi_lockable_currency_set_lock_work() { .execute_with(|| { assert_ok!(Tokens::set_lock(ID_1, DOT, &ALICE, 10)); assert_eq!(Tokens::accounts(&ALICE, DOT).frozen, 10); - assert_eq!(Tokens::accounts(&ALICE, DOT).frozen(), 10); + assert_eq!(Tokens::accounts(&ALICE, DOT).frozen, 10); assert_eq!(Tokens::locks(ALICE, DOT).len(), 1); assert_ok!(Tokens::set_lock(ID_1, DOT, &ALICE, 50)); assert_eq!(Tokens::accounts(&ALICE, DOT).frozen, 50); @@ -288,12 +288,6 @@ fn multi_reservable_currency_repatriate_reserved_work() { Tokens::repatriate_reserved(DOT, &ALICE, &ALICE, 50, BalanceStatus::Free), Ok(50) ); - // Repatriating from and to the same account, fund is `unreserved`. - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Unreserved { - currency_id: DOT, - who: ALICE, - amount: 0, - })); assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 0); @@ -350,7 +344,7 @@ fn multi_reservable_currency_repatriate_reserved_work() { } #[test] -fn slash_draw_reserved_correct() { +fn slash_cannot_draw_reserved() { ExtBuilder::default() .balances(vec![(ALICE, DOT, 100)]) .build() @@ -360,15 +354,15 @@ fn slash_draw_reserved_correct() { assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 50); assert_eq!(Tokens::total_issuance(DOT), 100); - assert_eq!(Tokens::slash(DOT, &ALICE, 80), 0); + assert_eq!(Tokens::slash(DOT, &ALICE, 80), 30); assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 20); - assert_eq!(Tokens::total_issuance(DOT), 20); + assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 50); + assert_eq!(Tokens::total_issuance(DOT), 50); - assert_eq!(Tokens::slash(DOT, &ALICE, 50), 30); + assert_eq!(Tokens::slash(DOT, &ALICE, 50), 50); assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::total_issuance(DOT), 0); + assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 50); + assert_eq!(Tokens::total_issuance(DOT), 50); }); } From 22ac4171f0ced7db9c4ec1d8635ca41e81ca77e1 Mon Sep 17 00:00:00 2001 From: wangjj9219 <183318287@qq.com> Date: Fri, 11 Aug 2023 00:13:11 +0800 Subject: [PATCH 10/11] fix events test --- tokens/src/impl_currency.rs | 12 +- tokens/src/impl_fungibles.rs | 28 +- tokens/src/impls.rs | 20 +- tokens/src/lib.rs | 8 +- tokens/src/tests.rs | 29 +- tokens/src/tests_events.rs | 798 ++++++++++++++++------------------- 6 files changed, 418 insertions(+), 477 deletions(-) diff --git a/tokens/src/impl_currency.rs b/tokens/src/impl_currency.rs index bd21287ee..a90f29f8a 100644 --- a/tokens/src/impl_currency.rs +++ b/tokens/src/impl_currency.rs @@ -241,7 +241,7 @@ impl MultiCurrency for Pallet { who, amount, ); - let remaining_slash = Self::mutate_account_handling_dust(currency_id, who, |account| -> Self::Balance { + Self::mutate_account_handling_dust(currency_id, who, |account| -> Self::Balance { let free_slashed_amount = account.free.min(amount); account.free = account.free.defensive_saturating_sub(free_slashed_amount); @@ -257,9 +257,7 @@ impl MultiCurrency for Pallet { }); amount.saturating_sub(free_slashed_amount) - }); - - remaining_slash + }) } } @@ -399,7 +397,7 @@ impl MultiReservableCurrency for Pallet { who, value, ); - let remaining_slash = Self::mutate_account_handling_dust(currency_id, who, |account| -> Self::Balance { + Self::mutate_account_handling_dust(currency_id, who, |account| -> Self::Balance { let reserved_slashed_amount = account.reserved.min(value); account.reserved = account.reserved.defensive_saturating_sub(reserved_slashed_amount); @@ -417,9 +415,7 @@ impl MultiReservableCurrency for Pallet { }); value.saturating_sub(reserved_slashed_amount) - }); - - remaining_slash + }) } fn reserved_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { diff --git a/tokens/src/impl_fungibles.rs b/tokens/src/impl_fungibles.rs index 976741469..e5aec3b31 100644 --- a/tokens/src/impl_fungibles.rs +++ b/tokens/src/impl_fungibles.rs @@ -223,15 +223,13 @@ impl fungibles::Unbalanced for Pallet { } else { Err(TokenError::BelowMinimum.into()) } + } else if new_balance == old_balance { + Ok(Self::Balance::default()) } else { - if new_balance == old_balance { - Ok(Self::Balance::default()) - } else { - if let Some(dust) = Self::write_balance(asset, who, new_balance)? { - Self::handle_dust(fungibles::Dust(asset, dust)); - } - Ok(new_balance.saturating_sub(old_balance)) + if let Some(dust) = Self::write_balance(asset, who, new_balance)? { + Self::handle_dust(fungibles::Dust(asset, dust)); } + Ok(new_balance.saturating_sub(old_balance)) } } } @@ -244,7 +242,7 @@ impl fungibles::Balanced for Pallet { Self::deposit_event(Event::::Deposited { currency_id: asset, who: who.clone(), - amount: amount, + amount, }); } @@ -252,7 +250,7 @@ impl fungibles::Balanced for Pallet { Self::deposit_event(Event::::Withdrawn { currency_id: asset, who: who.clone(), - amount: amount, + amount, }); } @@ -276,7 +274,7 @@ impl fungibles::Mutate for Pallet { Self::deposit_event(Event::::Deposited { currency_id: asset, who: who.clone(), - amount: amount, + amount, }); } @@ -284,7 +282,7 @@ impl fungibles::Mutate for Pallet { Self::deposit_event(Event::::Withdrawn { currency_id: asset, who: who.clone(), - amount: amount, + amount, }); } @@ -292,7 +290,7 @@ impl fungibles::Mutate for Pallet { Self::deposit_event(Event::::Withdrawn { currency_id: asset, who: who.clone(), - amount: amount, + amount, }); } @@ -300,7 +298,7 @@ impl fungibles::Mutate for Pallet { Self::deposit_event(Event::::Deposited { currency_id: asset, who: who.clone(), - amount: amount, + amount, }); } @@ -309,7 +307,7 @@ impl fungibles::Mutate for Pallet { currency_id: asset, from: source.clone(), to: dest.clone(), - amount: amount, + amount, }); } } @@ -445,7 +443,7 @@ impl fungibles::MutateHold for Pallet { currency_id: asset, from: source.clone(), to: dest.clone(), - amount: amount, + amount, status: BalanceStatus::Reserved, }); } diff --git a/tokens/src/impls.rs b/tokens/src/impls.rs index 36ece3e1b..c5731e0ff 100644 --- a/tokens/src/impls.rs +++ b/tokens/src/impls.rs @@ -1,8 +1,7 @@ use super::*; use frame_support::{ traits::{ - tokens::{Balance as BalanceT, Restriction}, - Currency as PalletCurrency, LockableCurrency as PalletLockableCurrency, + tokens::Balance as BalanceT, Currency as PalletCurrency, LockableCurrency as PalletLockableCurrency, NamedReservableCurrency as PalletNamedReservableCurrency, ReservableCurrency as PalletReservableCurrency, SignedImbalance, WithdrawReasons, }, @@ -752,7 +751,7 @@ where Pallet::::deposit_event(Event::::Deposited { currency_id: GetCurrencyId::get(), who: who.clone(), - amount: amount, + amount, }); } @@ -760,7 +759,7 @@ where Pallet::::deposit_event(Event::::Withdrawn { currency_id: GetCurrencyId::get(), who: who.clone(), - amount: amount, + amount, }); } @@ -768,7 +767,7 @@ where Pallet::::deposit_event(Event::::Withdrawn { currency_id: GetCurrencyId::get(), who: who.clone(), - amount: amount, + amount, }); } @@ -776,7 +775,7 @@ where Pallet::::deposit_event(Event::::Deposited { currency_id: GetCurrencyId::get(), who: who.clone(), - amount: amount, + amount, }); } @@ -785,7 +784,7 @@ where currency_id: GetCurrencyId::get(), from: source.clone(), to: dest.clone(), - amount: amount, + amount, }); } } @@ -834,7 +833,7 @@ where Pallet::::deposit_event(Event::::Deposited { currency_id: GetCurrencyId::get(), who: who.clone(), - amount: amount, + amount, }); } @@ -842,7 +841,7 @@ where Pallet::::deposit_event(Event::::Withdrawn { currency_id: GetCurrencyId::get(), who: who.clone(), - amount: amount, + amount, }); } @@ -861,7 +860,6 @@ where } } -type ReasonOfFungible =

::AccountId>>::Reason; impl fungible::InspectHold for CurrencyAdapter where T: Config, @@ -928,7 +926,7 @@ where currency_id: GetCurrencyId::get(), from: source.clone(), to: dest.clone(), - amount: amount, + amount, status: BalanceStatus::Reserved, }); } diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index 95edcfaa5..f1df0aed9 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -617,7 +617,7 @@ pub mod module { Self::deposit_event(Event::BalanceSet { currency_id, - who: who.clone(), + who, free: new_free, reserved: new_reserved, }); @@ -924,12 +924,12 @@ impl Pallet { .ok_or(Error::::BalanceTooLow)?; to_account.free = to_account.free.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; - let to_wipeout = Self::wipeout(currency_id, to, &to_account); + let to_wipeout = Self::wipeout(currency_id, to, to_account); ensure!(!to_wipeout, Error::::ExistentialDeposit); let allow_death = existence_requirement == ExistenceRequirement::AllowDeath; let allow_death = allow_death && frame_system::Pallet::::can_dec_provider(from); - let keep_alive = !Self::wipeout(currency_id, from, &from_account); + let keep_alive = !Self::wipeout(currency_id, from, from_account); ensure!(allow_death || keep_alive, Error::::KeepAlive); Ok(()) @@ -975,7 +975,7 @@ impl Pallet { Self::try_mutate_account_handling_dust(currency_id, who, |account, _| -> DispatchResult { account.free = account.free.defensive_saturating_sub(amount); - let keep_alive = !Self::wipeout(currency_id, who, &account); + let keep_alive = !Self::wipeout(currency_id, who, account); ensure!( existence_requirement == ExistenceRequirement::AllowDeath || keep_alive, Error::::KeepAlive diff --git a/tokens/src/tests.rs b/tokens/src/tests.rs index 303837b5f..e62411df6 100644 --- a/tokens/src/tests.rs +++ b/tokens/src/tests.rs @@ -969,7 +969,7 @@ fn do_withdraw_dust_removal_when_allow_death() { }); } -// #[test] +#[test] fn do_withdraw_report_keep_alive_error_when_ed_is_not_zero() { ExtBuilder::default() .balances(vec![(ALICE, DOT, 100), (DAVE, DOT, 100)]) @@ -995,13 +995,6 @@ fn do_withdraw_report_keep_alive_error_when_ed_is_not_zero() { assert!(Accounts::::contains_key(DAVE, DOT)); assert_eq!(Tokens::free_balance(DOT, &DAVE), 1); assert_eq!(Tokens::total_issuance(DOT), 101); - - // even if dave is in dust removal whitelist, but if withdraw all total of it - // will still cause account reaped. - assert_noop!( - Tokens::do_withdraw(DOT, &DAVE, 1, ExistenceRequirement::KeepAlive, true), - Error::::KeepAlive - ); }); } @@ -1642,7 +1635,7 @@ fn reap_account_will_dec_providers_work() { }); } -// #[test] +#[test] fn dust_removal_work() { ExtBuilder::default() .balances(vec![(ALICE, DOT, 100)]) @@ -1652,27 +1645,31 @@ fn dust_removal_work() { assert!(Accounts::::contains_key(ALICE, DOT)); assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 0); + assert_eq!(Tokens::total_issuance(DOT), 100); + + // set_balance cannot set free_balance below ED, will set 0 assert_ok!(Tokens::set_balance(RawOrigin::Root.into(), ALICE, DOT, 1, 0)); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::DustLost { + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::BalanceSet { currency_id: DOT, who: ALICE, - amount: 1, + free: 0, + reserved: 0, })); assert_eq!(System::providers(&ALICE), 0); assert!(!Accounts::::contains_key(ALICE, DOT)); assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 1); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 0); + assert_eq!(Tokens::total_issuance(DOT), 0); - // dave is in dust removal whitelist, will not remove its dust even if its free - // below ED - assert!(!Accounts::::contains_key(DAVE, DOT)); + // dave is in dust removal whitelist, will not wipeout assert_eq!(System::providers(&DAVE), 0); + assert!(!Accounts::::contains_key(DAVE, DOT)); assert_eq!(Tokens::free_balance(DOT, &DAVE), 0); assert_ok!(Tokens::set_balance(RawOrigin::Root.into(), DAVE, DOT, 1, 0)); assert!(Accounts::::contains_key(DAVE, DOT)); assert_eq!(System::providers(&DAVE), 1); assert_eq!(Tokens::free_balance(DOT, &DAVE), 1); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Endowed { + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Endowed { currency_id: DOT, who: DAVE, amount: 1, diff --git a/tokens/src/tests_events.rs b/tokens/src/tests_events.rs index 6d8bfae09..d28292111 100644 --- a/tokens/src/tests_events.rs +++ b/tokens/src/tests_events.rs @@ -5,10 +5,7 @@ use super::*; use frame_support::{ assert_ok, - traits::{ - tokens::Restriction, Currency as PalletCurrency, LockableCurrency as PalletLockableCurrency, - NamedReservableCurrency as PalletNamedReservableCurrency, ReservableCurrency as PalletReservableCurrency, - }, + traits::{tokens::Restriction, Currency as PalletCurrency}, }; use mock::*; @@ -21,430 +18,385 @@ fn events() -> Vec { } #[test] -fn test_fungible_events() { +fn pallet_multicurrency_deposit_events() { ExtBuilder::default() .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) .build() .execute_with(|| { - assert_eq!(>::shelve(&ALICE, 30), Ok(30)); - assert_eq!(System::events(), vec![]); + assert_ok!(>::transfer(DOT, &ALICE, &BOB, 10)); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Transfer { + currency_id: DOT, + from: ALICE, + to: BOB, + amount: 10, + })); + + assert_ok!(>::deposit(DOT, &ALICE, 10)); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Deposited { + currency_id: DOT, + who: ALICE, + amount: 10, + })); + + assert_ok!(>::withdraw(DOT, &ALICE, 10)); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Withdrawn { + currency_id: DOT, + who: ALICE, + amount: 10, + })); + + assert_ok!(>::reserve(DOT, &ALICE, 50)); + assert_eq!(>::slash(DOT, &ALICE, 60), 20); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Slashed { + currency_id: DOT, + who: ALICE, + free_amount: 40, + reserved_amount: 0, + })); + }); +} + +#[test] +fn pallet_multicurrency_extended_deposit_events() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) + .build() + .execute_with(|| { + assert_ok!(>::update_balance( + DOT, &ALICE, 500 + )); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Deposited { + currency_id: DOT, + who: ALICE, + amount: 500, + })); + assert_ok!(>::update_balance( + DOT, &ALICE, -500 + )); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Withdrawn { + currency_id: DOT, + who: ALICE, + amount: 500, + })); + }); +} + +#[test] +fn pallet_multi_lockable_currency_deposit_events() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100)]) + .build() + .execute_with(|| { + assert_ok!(>::set_lock( + [0u8; 8], DOT, &ALICE, 10 + )); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::LockSet { + lock_id: [0u8; 8], + currency_id: DOT, + who: ALICE, + amount: 10, + })); + + assert_ok!(>::remove_lock( + [0u8; 8], DOT, &ALICE + )); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::LockRemoved { + lock_id: [0u8; 8], + currency_id: DOT, + who: ALICE, + })); + }); +} + +#[test] +fn pallet_multi_reservable_currency_deposit_events() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 1000), (BOB, DOT, 1000)]) + .build() + .execute_with(|| { + assert_ok!(>::reserve( + DOT, &ALICE, 500 + )); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Reserved { + currency_id: DOT, + who: ALICE, + amount: 500, + })); + + assert_eq!( + >::slash_reserved(DOT, &ALICE, 300), + 0 + ); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Slashed { + currency_id: DOT, + who: ALICE, + free_amount: 0, + reserved_amount: 300, + })); + + assert_eq!( + >::unreserve(DOT, &ALICE, 100), + 0 + ); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Unreserved { + currency_id: DOT, + who: ALICE, + amount: 100, + })); + + assert_ok!(>::repatriate_reserved( + DOT, + &ALICE, + &BOB, + 100, + BalanceStatus::Free + )); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::ReserveRepatriated { + currency_id: DOT, + from: ALICE, + to: BOB, + amount: 100, + status: BalanceStatus::Free, + })); + }); +} + +#[test] +fn pallet_fungibles_mutate_deposit_events() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100)]) + .build() + .execute_with(|| { + assert_ok!(>::mint_into(DOT, &ALICE, 500)); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Deposited { + currency_id: DOT, + who: ALICE, + amount: 500, + })); + assert_ok!(>::burn_from( + DOT, + &ALICE, + 500, + Precision::Exact, + Fortitude::Polite + )); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Withdrawn { + currency_id: DOT, + who: ALICE, + amount: 500, + })); + }); +} - assert_eq!(>::shelve(DOT, &BOB, 30), Ok(30)); - //assert_eq!(System::events(), vec![]); +#[test] +fn pallet_fungibles_transfer_deposit_events() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) + .build() + .execute_with(|| { + assert_ok!(>::transfer( + DOT, + &ALICE, + &BOB, + 50, + Preservation::Protect + )); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Transfer { + currency_id: DOT, + from: ALICE, + to: BOB, + amount: 50, + })); }); } -// #[test] -// fn pallet_multicurrency_deposit_events() { -// ExtBuilder::default() -// .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) -// .build() -// .execute_with(|| { -// assert_ok!(>::transfer(DOT, &ALICE, &BOB, -// 10)); System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Transfer { -// currency_id: DOT, -// from: ALICE, -// to: BOB, -// amount: 10, -// })); - -// assert_ok!(>::deposit(DOT, &ALICE, 10)); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Deposited { -// currency_id: DOT, -// who: ALICE, -// amount: 10, -// })); - -// assert_ok!(>::withdraw(DOT, &ALICE, 10)); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Withdrawn { -// currency_id: DOT, -// who: ALICE, -// amount: 10, -// })); - -// assert_ok!(>::reserve(DOT, -// &ALICE, 50)); assert_eq!(>::slash(DOT, -// &ALICE, 60), 20); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Slashed { -// currency_id: DOT, -// who: ALICE, -// free_amount: 40, -// reserved_amount: 0, -// })); -// }); -// } - -// #[test] -// fn pallet_multicurrency_extended_deposit_events() { -// ExtBuilder::default() -// .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) -// .build() -// .execute_with(|| { -// assert_ok!(>::update_balance( -// DOT, &ALICE, 500 -// )); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Deposited { -// currency_id: DOT, -// who: ALICE, -// amount: 500, -// })); -// assert_ok!(>::update_balance( -// DOT, &ALICE, -500 -// )); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Withdrawn { -// currency_id: DOT, -// who: ALICE, -// amount: 500, -// })); -// }); -// } - -// #[test] -// fn pallet_multi_lockable_currency_deposit_events() { -// ExtBuilder::default() -// .balances(vec![(ALICE, DOT, 100)]) -// .build() -// .execute_with(|| { -// assert_ok!(>::set_lock( -// [0u8; 8], DOT, &ALICE, 10 -// )); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::LockSet { -// lock_id: [0u8; 8], -// currency_id: DOT, -// who: ALICE, -// amount: 10, -// })); - -// assert_ok!(>::remove_lock( -// [0u8; 8], DOT, &ALICE -// )); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::LockRemoved { -// lock_id: [0u8; 8], -// currency_id: DOT, -// who: ALICE, -// })); -// }); -// } - -// #[test] -// fn pallet_multi_reservable_currency_deposit_events() { -// ExtBuilder::default() -// .balances(vec![(ALICE, DOT, 1000), (BOB, DOT, 1000)]) -// .build() -// .execute_with(|| { -// assert_ok!(>::reserve( -// DOT, &ALICE, 500 -// )); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Reserved { -// currency_id: DOT, -// who: ALICE, -// amount: 500, -// })); - -// assert_eq!( -// >::slash_reserved(DOT, &ALICE, -// 300), 0 -// ); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Slashed { -// currency_id: DOT, -// who: ALICE, -// free_amount: 0, -// reserved_amount: 300, -// })); - -// assert_eq!( -// >::unreserve(DOT, &ALICE, 100), -// 0 -// ); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Unreserved { -// currency_id: DOT, -// who: ALICE, -// amount: 100, -// })); - -// assert_ok!(>::repatriate_reserved( DOT, -// &ALICE, -// &BOB, -// 100, -// BalanceStatus::Free -// )); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::ReserveRepatriated { -// currency_id: DOT, -// from: ALICE, -// to: BOB, -// amount: 100, -// status: BalanceStatus::Free, -// })); -// }); -// } - -// #[test] -// fn pallet_fungibles_mutate_deposit_events() { -// ExtBuilder::default() -// .balances(vec![(ALICE, DOT, 100)]) -// .build() -// .execute_with(|| { -// assert_ok!(>::mint_into(DOT, &ALICE, -// 500)); System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Deposited -// { currency_id: DOT, -// who: ALICE, -// amount: 500, -// })); -// assert_ok!(>::burn_from( -// DOT, -// &ALICE, -// 500, -// Precision::Exact, -// Fortitude::Polite -// )); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Withdrawn { -// currency_id: DOT, -// who: ALICE, -// amount: 500, -// })); -// }); -// } - -// #[test] -// fn pallet_fungibles_transfer_deposit_events() { -// ExtBuilder::default() -// .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) -// .build() -// .execute_with(|| { -// assert_ok!(>::transfer( -// DOT, -// &ALICE, -// &BOB, -// 50, -// Preservation::Protect -// )); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Transfer { -// currency_id: DOT, -// from: ALICE, -// to: BOB, -// amount: 50, -// })); -// }); -// } - -// #[test] -// fn pallet_fungibles_unbalanced_deposit_events() { -// ExtBuilder::default() -// .balances(vec![(ALICE, DOT, 100)]) -// .build() -// .execute_with(|| { -// assert_ok!(>::reserve(DOT, -// &ALICE, 50)); assert_ok!(>::write_balance( DOT, &ALICE, 500 -// )); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::BalanceSet { -// currency_id: DOT, -// who: ALICE, -// free: 500, -// reserved: 50, -// })); - -// >::set_total_issuance(DOT, 1000); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::TotalIssuanceSet -// { currency_id: DOT, -// amount: 1000, -// })); -// }); -// } - -// #[test] -// fn pallet_fungibles_mutate_hold_deposit_events() { -// ExtBuilder::default() -// .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) -// .build() -// .execute_with(|| { -// assert_ok!(>::hold( -// DOT, REASON, &ALICE, 50 -// )); -// assert_eq!(System::events(), vec![]); -// // System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Reserved { -// // currency_id: DOT, -// // who: ALICE, -// // amount: 50, -// // })); - -// // assert_ok!(>::transfer_on_hold( -// // DOT, -// // REASON, -// // &ALICE, -// // &BOB, -// // 50, -// // Precision::Exact, -// // Restriction::OnHold, -// // Fortitude::Polite -// // )); -// // System::assert_last_event(RuntimeEvent::Tokens(crate::Event::ReserveRepatriated -// { // currency_id: DOT, -// // from: ALICE, -// // to: BOB, -// // amount: 50, -// // status: BalanceStatus::Reserved, -// // })); -// // System::reset_events(); -// // assert_eq!( -// // >::release(DOT, REASON, &BOB, -// 50, Precision::Exact), // Ok(50) -// // ); -// // System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Unreserved { -// // currency_id: DOT, -// // who: BOB, -// // amount: 50, -// // })); -// }); -// } - -// #[test] -// fn currency_adapter_pallet_currency_deposit_events() { -// ExtBuilder::default() -// .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) -// .build() -// .execute_with(|| { -// // Use std::mem::forget to get rid the returned imbalance. -// std::mem::forget(>::burn(500)); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::TotalIssuanceSet -// { currency_id: DOT, -// amount: 0, -// })); - -// std::mem::forget(>::issue(200)); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::TotalIssuanceSet -// { currency_id: DOT, -// amount: 200, -// })); - -// assert_ok!(>::transfer( -// &ALICE, -// &BOB, -// 50, -// ExistenceRequirement::AllowDeath -// )); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Transfer { -// currency_id: DOT, -// from: ALICE, -// to: BOB, -// amount: 50, -// })); - -// assert_ok!(>::reserve(DOT, &BOB, -// 50)); std::mem::forget(>::slash(&BOB, 100)); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Slashed { -// currency_id: DOT, -// who: BOB, -// free_amount: 100, -// reserved_amount: 0, -// })); - -// std::mem::forget(>::make_free_balance_be(&BOB, 200)); -// System::assert_last_event(RuntimeEvent::Tokens(crate::Event::BalanceSet { -// currency_id: DOT, -// who: BOB, -// free: 200, -// reserved: 50, -// })); -// }); -// } - -// #[test] -// fn pallet_change_locks_events() { -// ExtBuilder::default().build().execute_with(|| { -// assert_ok!(Tokens::do_deposit(DOT, &ALICE, 100, false, false)); -// assert_ok!(Tokens::do_deposit(BTC, &ALICE, 100, false, false)); -// System::reset_events(); - -// // Locks: [10/DOT] -// assert_ok!(Tokens::set_lock(ID_1, DOT, &ALICE, 10)); -// assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Locked { -// currency_id: DOT, -// who: ALICE, -// amount: 10 -// }))); - -// // Locks: [15/DOT] -// assert_ok!(Tokens::set_lock(ID_1, DOT, &ALICE, 15)); -// assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Locked { -// currency_id: DOT, -// who: ALICE, -// amount: 5 -// }))); - -// // Locks: [15/DOT, 20/BTC] -// assert_ok!(Tokens::set_lock(ID_1, BTC, &ALICE, 20)); -// assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Locked { -// currency_id: BTC, -// who: ALICE, -// amount: 20 -// }))); - -// // Locks: [15/DOT, 20/BTC, 10/DOT] -// assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 10)); -// for event in events() { -// match event { -// RuntimeEvent::Tokens(crate::Event::Locked { .. }) => assert!(false, -// "unexpected lock event"), RuntimeEvent::Tokens(crate::Event::Unlocked { .. }) -// => assert!(false, "unexpected unlock event"), _ => continue, -// } -// } - -// // Locks: [15/DOT, 20/BTC, 12/DOT] -// assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 12)); -// for event in events() { -// match event { -// RuntimeEvent::Tokens(crate::Event::Locked { .. }) => assert!(false, -// "unexpected lock event"), RuntimeEvent::Tokens(crate::Event::Unlocked { .. }) -// => assert!(false, "unexpected unlock event"), _ => continue, -// } -// } - -// // Locks: [15/DOT, 20/BTC, 10/DOT] -// assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 10)); -// for event in events() { -// match event { -// RuntimeEvent::Tokens(crate::Event::Locked { .. }) => assert!(false, -// "unexpected lock event"), RuntimeEvent::Tokens(crate::Event::Unlocked { .. }) -// => assert!(false, "unexpected unlock event"), _ => continue, -// } -// } - -// // Locks: [15/DOT, 20/BTC, 20/DOT] -// assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 20)); -// assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Locked { -// currency_id: DOT, -// who: ALICE, -// amount: 5 -// }))); - -// // Locks: [15/DOT, 20/BTC, 16/DOT] -// assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 16)); -// assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Unlocked { -// currency_id: DOT, -// who: ALICE, -// amount: 4 -// }))); - -// // Locks: [15/DOT, 12/BTC, 16/DOT] -// assert_ok!(Tokens::set_lock(ID_1, BTC, &ALICE, 12)); -// assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Unlocked { -// currency_id: BTC, -// who: ALICE, -// amount: 8 -// }))); - -// // Locks: [15/DOT, 12/BTC] -// assert_ok!(Tokens::remove_lock(ID_2, DOT, &ALICE)); -// assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Unlocked { -// currency_id: DOT, -// who: ALICE, -// amount: 1 -// }))); -// }); -// } +#[test] +fn pallet_fungibles_mutate_hold_deposit_events() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) + .build() + .execute_with(|| { + assert_ok!(>::hold( + DOT, REASON, &ALICE, 50 + )); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Reserved { + currency_id: DOT, + who: ALICE, + amount: 50, + })); + + assert_ok!(>::transfer_on_hold( + DOT, + REASON, + &ALICE, + &BOB, + 50, + Precision::Exact, + Restriction::OnHold, + Fortitude::Polite + )); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::ReserveRepatriated { + currency_id: DOT, + from: ALICE, + to: BOB, + amount: 50, + status: BalanceStatus::Reserved, + })); + System::reset_events(); + assert_eq!( + >::release(DOT, REASON, &BOB, 50, Precision::Exact), + Ok(50) + ); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Unreserved { + currency_id: DOT, + who: BOB, + amount: 50, + })); + }); +} + +#[test] +fn currency_adapter_pallet_currency_deposit_events() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) + .build() + .execute_with(|| { + // Use std::mem::forget to get rid the returned imbalance. + std::mem::forget(>::burn(500)); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::TotalIssuanceSet { + currency_id: DOT, + amount: 0, + })); + + std::mem::forget(>::issue(200)); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::TotalIssuanceSet { + currency_id: DOT, + amount: 200, + })); + + assert_ok!(>::transfer( + &ALICE, + &BOB, + 50, + ExistenceRequirement::AllowDeath + )); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Transfer { + currency_id: DOT, + from: ALICE, + to: BOB, + amount: 50, + })); + + assert_ok!(>::reserve(DOT, &BOB, 50)); + std::mem::forget(>::slash(&BOB, 100)); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Slashed { + currency_id: DOT, + who: BOB, + free_amount: 100, + reserved_amount: 0, + })); + + std::mem::forget(>::make_free_balance_be(&BOB, 200)); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::BalanceSet { + currency_id: DOT, + who: BOB, + free: 200, + reserved: 50, + })); + }); +} + +#[test] +fn pallet_change_locks_events() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(Tokens::do_deposit(DOT, &ALICE, 100, false, false)); + assert_ok!(Tokens::do_deposit(BTC, &ALICE, 100, false, false)); + System::reset_events(); + + // Locks: [10/DOT] + assert_ok!(Tokens::set_lock(ID_1, DOT, &ALICE, 10)); + assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Locked { + currency_id: DOT, + who: ALICE, + amount: 10 + }))); + + // Locks: [15/DOT] + assert_ok!(Tokens::set_lock(ID_1, DOT, &ALICE, 15)); + assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Locked { + currency_id: DOT, + who: ALICE, + amount: 5 + }))); + + // Locks: [15/DOT, 20/BTC] + assert_ok!(Tokens::set_lock(ID_1, BTC, &ALICE, 20)); + assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Locked { + currency_id: BTC, + who: ALICE, + amount: 20 + }))); + + // Locks: [15/DOT, 20/BTC, 10/DOT] + assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 10)); + for event in events() { + match event { + RuntimeEvent::Tokens(crate::Event::Locked { .. }) => assert!(false, "unexpected lock event"), + RuntimeEvent::Tokens(crate::Event::Unlocked { .. }) => assert!(false, "unexpected unlock event"), + _ => continue, + } + } + + // Locks: [15/DOT, 20/BTC, 12/DOT] + assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 12)); + for event in events() { + match event { + RuntimeEvent::Tokens(crate::Event::Locked { .. }) => assert!(false, "unexpected lock event"), + RuntimeEvent::Tokens(crate::Event::Unlocked { .. }) => assert!(false, "unexpected unlock event"), + _ => continue, + } + } + + // Locks: [15/DOT, 20/BTC, 10/DOT] + assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 10)); + for event in events() { + match event { + RuntimeEvent::Tokens(crate::Event::Locked { .. }) => assert!(false, "unexpected lock event"), + RuntimeEvent::Tokens(crate::Event::Unlocked { .. }) => assert!(false, "unexpected unlock event"), + _ => continue, + } + } + + // Locks: [15/DOT, 20/BTC, 20/DOT] + assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 20)); + assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Locked { + currency_id: DOT, + who: ALICE, + amount: 5 + }))); + + // Locks: [15/DOT, 20/BTC, 16/DOT] + assert_ok!(Tokens::set_lock(ID_2, DOT, &ALICE, 16)); + assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Unlocked { + currency_id: DOT, + who: ALICE, + amount: 4 + }))); + + // Locks: [15/DOT, 12/BTC, 16/DOT] + assert_ok!(Tokens::set_lock(ID_1, BTC, &ALICE, 12)); + assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Unlocked { + currency_id: BTC, + who: ALICE, + amount: 8 + }))); + + // Locks: [15/DOT, 12/BTC] + assert_ok!(Tokens::remove_lock(ID_2, DOT, &ALICE)); + assert!(events().contains(&RuntimeEvent::Tokens(crate::Event::Unlocked { + currency_id: DOT, + who: ALICE, + amount: 1 + }))); + }); +} From e9b684d87ec3e684dd48974629a9a474f4b7f580 Mon Sep 17 00:00:00 2001 From: wangjj9219 <183318287@qq.com> Date: Tue, 15 Aug 2023 20:28:09 +0800 Subject: [PATCH 11/11] add conformance for fungibles --- tokens/Cargo.toml | 1 + .../inspect_mutate.rs | 1119 +++++++++++++++++ tokens/src/fungibles_conformance_tests/mod.rs | 3 + tokens/src/lib.rs | 2 + tokens/src/tests_fungibles_conformance.rs | 75 ++ 5 files changed, 1200 insertions(+) create mode 100644 tokens/src/fungibles_conformance_tests/inspect_mutate.rs create mode 100644 tokens/src/fungibles_conformance_tests/mod.rs create mode 100644 tokens/src/tests_fungibles_conformance.rs diff --git a/tokens/Cargo.toml b/tokens/Cargo.toml index 6fa9c3e8f..0cc857d5c 100644 --- a/tokens/Cargo.toml +++ b/tokens/Cargo.toml @@ -26,6 +26,7 @@ pallet-treasury = { git = "https://github.com/paritytech/substrate", branch = "p sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v1.0.0" } sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v1.0.0" } sp-staking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v1.0.0" } +paste = "1.0.12" [features] default = ["std"] diff --git a/tokens/src/fungibles_conformance_tests/inspect_mutate.rs b/tokens/src/fungibles_conformance_tests/inspect_mutate.rs new file mode 100644 index 000000000..2210b5d24 --- /dev/null +++ b/tokens/src/fungibles_conformance_tests/inspect_mutate.rs @@ -0,0 +1,1119 @@ +use super::*; +use core::fmt::Debug; +use frame_support::traits::{ + fungibles::{Inspect, Mutate}, + tokens::{DepositConsequence, Fortitude, Precision, Preservation, Provenance, WithdrawConsequence}, +}; +use sp_arithmetic::traits::AtLeast8BitUnsigned; +use sp_runtime::traits::{Bounded, Zero}; + +/// Test the `mint_into` function for successful token minting. +/// +/// This test checks the `mint_into` function in the `Mutate` trait +/// implementation for type `T`. It ensures that account balances and total +/// issuance values are updated correctly after minting tokens into two distinct +/// accounts. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn mint_into_success(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account_0 = AccountId::from([10u8; 32]); + let account_1 = AccountId::from([11u8; 32]); + + // Test: Mint an amount into each account + let amount_0 = T::minimum_balance(asset_id); + let amount_1 = T::minimum_balance(asset_id) + 5.into(); + T::mint_into(asset_id, &account_0, amount_0).unwrap(); + T::mint_into(asset_id, &account_1, amount_1).unwrap(); + + // Verify: Account balances are updated correctly + assert_eq!(T::total_balance(asset_id, &account_0), amount_0); + assert_eq!(T::total_balance(asset_id, &account_1), amount_1); + assert_eq!(T::balance(asset_id, &account_0), amount_0); + assert_eq!(T::balance(asset_id, &account_1), amount_1); + + // Verify: Total issuance is updated correctly + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + amount_0 + amount_1 + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + amount_0 + amount_1 + ); +} + +/// Test the `mint_into` function for overflow prevention. +/// +/// This test ensures that minting tokens beyond the maximum balance value for +/// an account returns an error and does not change the account balance or total +/// issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn mint_into_overflow(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account = AccountId::from([10u8; 32]); + let amount = T::Balance::max_value() - 5.into() - initial_total_issuance; + + // Mint just below the maximum balance + T::mint_into(asset_id, &account, amount).unwrap(); + + // Verify: Minting beyond the maximum balance value returns an Err + T::mint_into(asset_id, &account, 10.into()).unwrap_err(); + + // Verify: The balance did not change + assert_eq!(T::total_balance(asset_id, &account), amount); + assert_eq!(T::balance(asset_id, &account), amount); + + // Verify: The total issuance did not change + assert_eq!(T::total_issuance(asset_id), initial_total_issuance + amount); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance + amount); +} + +/// Test the `mint_into` function for handling balances below the minimum value. +/// +/// This test verifies that minting tokens below the minimum balance for an +/// account returns an error and has no impact on the account balance or total +/// issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn mint_into_below_minimum( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + // Skip if there is no minimum balance + if T::minimum_balance(asset_id) == T::Balance::zero() { + return; + } + + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account = AccountId::from([10u8; 32]); + let amount = T::minimum_balance(asset_id) - 1.into(); + + // Verify: Minting below the minimum balance returns Err + T::mint_into(asset_id, &account, amount).unwrap_err(); + + // Verify: noop + assert_eq!(T::total_balance(asset_id, &account), T::Balance::zero()); + assert_eq!(T::balance(asset_id, &account), T::Balance::zero()); + assert_eq!(T::total_issuance(asset_id), initial_total_issuance); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance); +} + +/// Test the `burn_from` function for successfully burning an exact amount of +/// tokens. +/// +/// This test checks that the `burn_from` function with `Precision::Exact` +/// correctly reduces the account balance and total issuance values by the +/// burned amount. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate` for `AccountId`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn burn_from_exact_success( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + + // Setup account + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Test: Burn an exact amount from the account + let amount_to_burn = T::Balance::from(5); + let precision = Precision::Exact; + let force = Fortitude::Polite; + T::burn_from(asset_id, &account, amount_to_burn, precision, force).unwrap(); + + // Verify: The balance and total issuance should be reduced by the burned amount + assert_eq!(T::balance(asset_id, &account), initial_balance - amount_to_burn); + assert_eq!(T::total_balance(asset_id, &account), initial_balance - amount_to_burn); + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + initial_balance - amount_to_burn + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + initial_balance - amount_to_burn + ); +} + +/// Test the `burn_from` function for successfully burning tokens with a +/// best-effort approach. +/// +/// This test verifies that the `burn_from` function with +/// `Precision::BestEffort` correctly reduces the account balance and total +/// issuance values by the reducible balance when attempting to burn an amount +/// greater than the reducible balance. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate` for `AccountId`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn burn_from_best_effort_success( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + + // Setup account + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Get reducible balance + let force = Fortitude::Polite; + let reducible_balance = T::reducible_balance(asset_id, &account, Preservation::Expendable, force); + + // Test: Burn a best effort amount from the account that is greater than the + // reducible balance + let amount_to_burn = reducible_balance + 5.into(); + let precision = Precision::BestEffort; + assert!(amount_to_burn > reducible_balance); + assert!(amount_to_burn > T::balance(asset_id, &account)); + T::burn_from(asset_id, &account, amount_to_burn, precision, force).unwrap(); + + // Verify: The balance and total issuance should be reduced by the + // reducible_balance + assert_eq!(T::balance(asset_id, &account), initial_balance - reducible_balance); + assert_eq!( + T::total_balance(asset_id, &account), + initial_balance - reducible_balance + ); + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + initial_balance - reducible_balance + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + initial_balance - reducible_balance + ); +} + +/// Test the `burn_from` function for handling insufficient funds with +/// `Precision::Exact`. +/// +/// This test verifies that burning an amount greater than the account's balance +/// with `Precision::Exact` returns an error and does not change the account +/// balance or total issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn burn_from_exact_insufficient_funds( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + // Set up the initial conditions and parameters for the test + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + + // Verify: Burn an amount greater than the account's balance with Exact + // precision returns Err + let amount_to_burn = initial_balance + 10.into(); + let precision = Precision::Exact; + let force = Fortitude::Polite; + T::burn_from(asset_id, &account, amount_to_burn, precision, force).unwrap_err(); + + // Verify: The balance and total issuance should remain unchanged + assert_eq!(T::balance(asset_id, &account), initial_balance); + assert_eq!(T::total_balance(asset_id, &account), initial_balance); + assert_eq!(T::total_issuance(asset_id), initial_total_issuance); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance); +} + +/// Test the `restore` function for successful restoration. +/// +/// This test verifies that restoring an amount into each account updates their +/// balances and the total issuance values correctly. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn restore_success(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let account_0 = AccountId::from([10u8; 32]); + let account_1 = AccountId::from([11u8; 32]); + + // Test: Restore an amount into each account + let amount_0 = T::minimum_balance(asset_id); + let amount_1 = T::minimum_balance(asset_id) + 5.into(); + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + T::restore(asset_id, &account_0, amount_0).unwrap(); + T::restore(asset_id, &account_1, amount_1).unwrap(); + + // Verify: Account balances are updated correctly + assert_eq!(T::total_balance(asset_id, &account_0), amount_0); + assert_eq!(T::total_balance(asset_id, &account_1), amount_1); + assert_eq!(T::balance(asset_id, &account_0), amount_0); + assert_eq!(T::balance(asset_id, &account_1), amount_1); + + // Verify: Total issuance is updated correctly + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + amount_0 + amount_1 + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + amount_0 + amount_1 + ); +} + +/// Test the `restore` function for handling balance overflow. +/// +/// This test verifies that restoring an amount beyond the maximum balance +/// returns an error and does not change the account balance or total issuance +/// values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn restore_overflow(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account = AccountId::from([10u8; 32]); + let amount = T::Balance::max_value() - 5.into() - initial_total_issuance; + + // Restore just below the maximum balance + T::restore(asset_id, &account, amount).unwrap(); + + // Verify: Restoring beyond the maximum balance returns an Err + T::restore(asset_id, &account, 10.into()).unwrap_err(); + + // Verify: The balance and total issuance did not change + assert_eq!(T::total_balance(asset_id, &account), amount); + assert_eq!(T::balance(asset_id, &account), amount); + assert_eq!(T::total_issuance(asset_id), initial_total_issuance + amount); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance + amount); +} + +/// Test the `restore` function for handling restoration below the minimum +/// balance. +/// +/// This test verifies that restoring an amount below the minimum balance +/// returns an error and does not change the account balance or total issuance +/// values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn restore_below_minimum(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + // Skip if there is no minimum balance + if T::minimum_balance(asset_id) == T::Balance::zero() { + return; + } + + let account = AccountId::from([10u8; 32]); + let amount = T::minimum_balance(asset_id) - 1.into(); + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + + // Verify: Restoring below the minimum balance returns Err + T::restore(asset_id, &account, amount).unwrap_err(); + + // Verify: noop + assert_eq!(T::total_balance(asset_id, &account), T::Balance::zero()); + assert_eq!(T::balance(asset_id, &account), T::Balance::zero()); + assert_eq!(T::total_issuance(asset_id), initial_total_issuance); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance); +} + +/// Test the `shelve` function for successful shelving. +/// +/// This test verifies that shelving an amount from an account reduces the +/// account balance and total issuance values by the shelved amount. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn shelve_success(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + + // Setup account + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + + T::restore(asset_id, &account, initial_balance).unwrap(); + + // Test: Shelve an amount from the account + let amount_to_shelve = T::Balance::from(5); + T::shelve(asset_id, &account, amount_to_shelve).unwrap(); + + // Verify: The balance and total issuance should be reduced by the shelved + // amount + assert_eq!(T::balance(asset_id, &account), initial_balance - amount_to_shelve); + assert_eq!(T::total_balance(asset_id, &account), initial_balance - amount_to_shelve); + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + initial_balance - amount_to_shelve + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + initial_balance - amount_to_shelve + ); +} + +/// Test the `shelve` function for handling insufficient funds. +/// +/// This test verifies that attempting to shelve an amount greater than the +/// account's balance returns an error and does not change the account balance +/// or total issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn shelve_insufficient_funds( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + + // Set up the initial conditions and parameters for the test + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::restore(asset_id, &account, initial_balance).unwrap(); + + // Verify: Shelving greater than the balance with Exact precision returns Err + let amount_to_shelve = initial_balance + 10.into(); + T::shelve(asset_id, &account, amount_to_shelve).unwrap_err(); + + // Verify: The balance and total issuance should remain unchanged + assert_eq!(T::balance(asset_id, &account), initial_balance); + assert_eq!(T::total_balance(asset_id, &account), initial_balance); + assert_eq!(T::total_issuance(asset_id), initial_total_issuance + initial_balance); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance + initial_balance); +} + +/// Test the `transfer` function for a successful transfer. +/// +/// This test verifies that transferring an amount between two accounts with +/// `Preservation::Expendable` updates the account balances and maintains the +/// total issuance and active issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn transfer_success(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account_0 = AccountId::from([10u8; 32]); + let account_1 = AccountId::from([11u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::set_balance(asset_id, &account_0, initial_balance); + T::set_balance(asset_id, &account_1, initial_balance); + + // Test: Transfer an amount from account_0 to account_1 + let transfer_amount = T::Balance::from(3); + T::transfer( + asset_id, + &account_0, + &account_1, + transfer_amount, + Preservation::Expendable, + ) + .unwrap(); + + // Verify: Account balances are updated correctly + assert_eq!( + T::total_balance(asset_id, &account_0), + initial_balance - transfer_amount + ); + assert_eq!( + T::total_balance(asset_id, &account_1), + initial_balance + transfer_amount + ); + assert_eq!(T::balance(asset_id, &account_0), initial_balance - transfer_amount); + assert_eq!(T::balance(asset_id, &account_1), initial_balance + transfer_amount); + + // Verify: Total issuance doesn't change + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + initial_balance * 2.into() + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + initial_balance * 2.into() + ); +} + +/// Test the `transfer` function with `Preservation::Expendable` for +/// transferring the entire balance. +/// +/// This test verifies that transferring the entire balance from one account to +/// another with `Preservation::Expendable` updates the account balances and +/// maintains the total issuance and active issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn transfer_expendable_all( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account_0 = AccountId::from([10u8; 32]); + let account_1 = AccountId::from([11u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::set_balance(asset_id, &account_0, initial_balance); + T::set_balance(asset_id, &account_1, initial_balance); + + // Test: Transfer entire balance from account_0 to account_1 + let preservation = Preservation::Expendable; + let transfer_amount = initial_balance; + T::transfer(asset_id, &account_0, &account_1, transfer_amount, preservation).unwrap(); + + // Verify: Account balances are updated correctly + assert_eq!(T::total_balance(asset_id, &account_0), T::Balance::zero()); + assert_eq!(T::total_balance(asset_id, &account_1), initial_balance * 2.into()); + assert_eq!(T::balance(asset_id, &account_0), T::Balance::zero()); + assert_eq!(T::balance(asset_id, &account_1), initial_balance * 2.into()); + + // Verify: Total issuance doesn't change + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + initial_balance * 2.into() + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + initial_balance * 2.into() + ); +} + +/// Test the transfer function with Preservation::Expendable for transferring +/// amounts that leaves an account with less than the minimum balance. +/// +/// This test verifies that when transferring an amount using +/// Preservation::Expendable and an account will be left with less than the +/// minimum balance, the account balances are updated, dust is collected +/// properly depending on whether a dust_trap exists, and the total issuance and +/// active issuance values remain consistent. +/// +/// # Parameters +/// +/// - dust_trap: An optional account identifier to which dust will be collected. +/// If None, dust will be removed from the total and active issuance. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn transfer_expendable_dust( + asset_id: >::AssetId, + dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + if T::minimum_balance(asset_id) == T::Balance::zero() { + return; + } + + let account_0 = AccountId::from([10u8; 32]); + let account_1 = AccountId::from([20u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::set_balance(asset_id, &account_0, initial_balance); + T::set_balance(asset_id, &account_1, initial_balance); + + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let initial_dust_trap_balance = match dust_trap { + Some(ref dust_trap) => T::total_balance(asset_id, &dust_trap), + None => T::Balance::zero(), + }; + + // Test: Transfer balance + let preservation = Preservation::Expendable; + let transfer_amount = T::Balance::from(11); + T::transfer(asset_id, &account_0, &account_1, transfer_amount, preservation).unwrap(); + + // Verify: Account balances are updated correctly + assert_eq!(T::total_balance(asset_id, &account_0), T::Balance::zero()); + assert_eq!( + T::total_balance(asset_id, &account_1), + initial_balance + transfer_amount + ); + assert_eq!(T::balance(asset_id, &account_0), T::Balance::zero()); + assert_eq!(T::balance(asset_id, &account_1), initial_balance + transfer_amount); + + match dust_trap { + Some(ref dust_trap) => { + // Verify: Total issuance and active issuance don't change + assert_eq!(T::total_issuance(asset_id), initial_total_issuance); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance); + // Verify: Dust is collected into dust trap + assert_eq!( + T::total_balance(asset_id, &dust_trap), + initial_dust_trap_balance + T::minimum_balance(asset_id) - 1.into() + ); + assert_eq!( + T::balance(asset_id, &dust_trap), + initial_dust_trap_balance + T::minimum_balance(asset_id) - 1.into() + ); + } + None => { + // Verify: Total issuance and active issuance are reduced by the dust amount + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance - T::minimum_balance(asset_id) + 1.into() + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance - T::minimum_balance(asset_id) + 1.into() + ); + } + } +} + +/// Test the `transfer` function with `Preservation::Protect` and +/// `Preservation::Preserve` for transferring the entire balance. +/// +/// This test verifies that attempting to transfer the entire balance with +/// `Preservation::Protect` or `Preservation::Preserve` returns an error, and +/// the account balances, total issuance, and active issuance values remain +/// unchanged. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn transfer_protect_preserve( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + // This test means nothing if there is no minimum balance + if T::minimum_balance(asset_id) == T::Balance::zero() { + return; + } + + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account_0 = AccountId::from([10u8; 32]); + let account_1 = AccountId::from([11u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::set_balance(asset_id, &account_0, initial_balance); + T::set_balance(asset_id, &account_1, initial_balance); + + // Verify: Transfer Protect entire balance from account_0 to account_1 should + // Err + let preservation = Preservation::Protect; + let transfer_amount = initial_balance; + T::transfer(asset_id, &account_0, &account_1, transfer_amount, preservation).unwrap_err(); + + // Verify: Noop + assert_eq!(T::total_balance(asset_id, &account_0), initial_balance); + assert_eq!(T::total_balance(asset_id, &account_1), initial_balance); + assert_eq!(T::balance(asset_id, &account_0), initial_balance); + assert_eq!(T::balance(asset_id, &account_1), initial_balance); + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + initial_balance * 2.into() + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + initial_balance * 2.into() + ); + + // Verify: Transfer Preserve entire balance from account_0 to account_1 should + // Err + let preservation = Preservation::Preserve; + T::transfer(asset_id, &account_0, &account_1, transfer_amount, preservation).unwrap_err(); + + // Verify: Noop + assert_eq!(T::total_balance(asset_id, &account_0), initial_balance); + assert_eq!(T::total_balance(asset_id, &account_1), initial_balance); + assert_eq!(T::balance(asset_id, &account_0), initial_balance); + assert_eq!(T::balance(asset_id, &account_1), initial_balance); + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + initial_balance * 2.into() + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + initial_balance * 2.into() + ); +} + +/// Test the set_balance function for successful minting. +/// +/// This test verifies that minting a balance using set_balance updates the +/// account balance, total issuance, and active issuance correctly. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn set_balance_mint_success( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Test: Increase the account balance with set_balance + let increase_amount: T::Balance = 5.into(); + let new = T::set_balance(asset_id, &account, initial_balance + increase_amount); + + // Verify: set_balance returned the new balance + let expected_new = initial_balance + increase_amount; + assert_eq!(new, expected_new); + + // Verify: Balance and issuance is updated correctly + assert_eq!(T::total_balance(asset_id, &account), expected_new); + assert_eq!(T::balance(asset_id, &account), expected_new); + assert_eq!(T::total_issuance(asset_id), initial_total_issuance + expected_new); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance + expected_new); +} + +/// Test the set_balance function for successful burning. +/// +/// This test verifies that burning a balance using set_balance updates the +/// account balance, total issuance, and active issuance correctly. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn set_balance_burn_success( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Test: Increase the account balance with set_balance + let burn_amount: T::Balance = 5.into(); + let new = T::set_balance(asset_id, &account, initial_balance - burn_amount); + + // Verify: set_balance returned the new balance + let expected_new = initial_balance - burn_amount; + assert_eq!(new, expected_new); + + // Verify: Balance and issuance is updated correctly + assert_eq!(T::total_balance(asset_id, &account), expected_new); + assert_eq!(T::balance(asset_id, &account), expected_new); + assert_eq!(T::total_issuance(asset_id), initial_total_issuance + expected_new); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance + expected_new); +} + +/// Test the can_deposit function for returning a success value. +/// +/// This test verifies that the can_deposit function returns +/// DepositConsequence::Success when depositing a reasonable amount. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_deposit_success(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Test: can_deposit a reasonable amount + let ret = T::can_deposit(asset_id, &account, 5.into(), Provenance::Minted); + + // Verify: Returns success + assert_eq!(ret, DepositConsequence::Success); +} + +/// Test the can_deposit function for returning a minimum balance error. +/// +/// This test verifies that the can_deposit function returns +/// DepositConsequence::BelowMinimum when depositing below the minimum balance. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_deposit_below_minimum( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + // can_deposit always returns Success for amount 0 + if T::minimum_balance(asset_id) < 2.into() { + return; + } + + let account = AccountId::from([10u8; 32]); + + // Test: can_deposit below the minimum + let ret = T::can_deposit( + asset_id, + &account, + T::minimum_balance(asset_id) - 1.into(), + Provenance::Minted, + ); + + // Verify: Returns success + assert_eq!(ret, DepositConsequence::BelowMinimum); +} + +/// Test the can_deposit function for returning an overflow error. +/// +/// This test verifies that the can_deposit function returns +/// DepositConsequence::Overflow when depositing an amount that would cause an +/// overflow. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_deposit_overflow(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let account = AccountId::from([10u8; 32]); + + // Test: Try deposit over the max balance + let initial_balance = T::Balance::max_value() - 5.into() - T::total_issuance(asset_id); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + let ret = T::can_deposit(asset_id, &account, 10.into(), Provenance::Minted); + + // Verify: Returns success + assert_eq!(ret, DepositConsequence::Overflow); +} + +/// Test the can_withdraw function for returning a success value. +/// +/// This test verifies that the can_withdraw function returns +/// WithdrawConsequence::Success when withdrawing a reasonable amount. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_withdraw_success(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Test: can_withdraw a reasonable amount + let ret = T::can_withdraw(asset_id, &account, 5.into()); + + // Verify: Returns success + assert_eq!(ret, WithdrawConsequence::Success); +} + +/// Test the can_withdraw function for withdrawal resulting in a reduced balance +/// of zero. +/// +/// This test verifies that the can_withdraw function returns +/// WithdrawConsequence::ReducedToZero when withdrawing an amount that would +/// reduce the account balance below the minimum balance. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_withdraw_reduced_to_zero( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + if T::minimum_balance(asset_id) == T::Balance::zero() { + return; + } + + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Verify: can_withdraw below the minimum balance returns ReducedToZero + let ret = T::can_withdraw(asset_id, &account, 1.into()); + assert_eq!( + ret, + WithdrawConsequence::ReducedToZero(T::minimum_balance(asset_id) - 1.into()) + ); +} + +/// Test the can_withdraw function for returning a low balance error. +/// +/// This test verifies that the can_withdraw function returns +/// WithdrawConsequence::BalanceLow when withdrawing an amount that would result +/// in an account balance below the current balance. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_withdraw_balance_low( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + if T::minimum_balance(asset_id) == T::Balance::zero() { + return; + } + + let account = AccountId::from([10u8; 32]); + let other_account = AccountId::from([100u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 5.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + T::mint_into(asset_id, &other_account, initial_balance * 2.into()).unwrap(); + + // Verify: can_withdraw below the account balance returns BalanceLow + let ret = T::can_withdraw(asset_id, &account, initial_balance + 1.into()); + assert_eq!(ret, WithdrawConsequence::BalanceLow); +} + +/// Test the reducible_balance function with Preservation::Expendable. +/// +/// This test verifies that the reducible_balance function returns the full +/// account balance when using Preservation::Expendable. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn reducible_balance_expendable( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Verify: reducible_balance returns the full balance + let ret = T::reducible_balance(asset_id, &account, Preservation::Expendable, Fortitude::Polite); + assert_eq!(ret, initial_balance); +} + +/// Test the reducible_balance function with Preservation::Protect and +/// Preservation::Preserve. +/// +/// This test verifies that the reducible_balance function returns the account +/// balance minus the minimum balance when using either Preservation::Protect or +/// Preservation::Preserve. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn reducible_balance_protect_preserve( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Verify: reducible_balance returns the full balance - min balance + let ret = T::reducible_balance(asset_id, &account, Preservation::Protect, Fortitude::Polite); + assert_eq!(ret, initial_balance - T::minimum_balance(asset_id)); + let ret = T::reducible_balance(asset_id, &account, Preservation::Preserve, Fortitude::Polite); + assert_eq!(ret, initial_balance - T::minimum_balance(asset_id)); +} diff --git a/tokens/src/fungibles_conformance_tests/mod.rs b/tokens/src/fungibles_conformance_tests/mod.rs new file mode 100644 index 000000000..291223473 --- /dev/null +++ b/tokens/src/fungibles_conformance_tests/mod.rs @@ -0,0 +1,3 @@ +#![cfg(test)] + +pub mod inspect_mutate; diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index f1df0aed9..10f18f35f 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -69,6 +69,7 @@ use orml_traits::{ MultiReservableCurrency, NamedMultiReservableCurrency, }; +mod fungibles_conformance_tests; mod impl_currency; mod impl_fungibles; mod impls; @@ -77,6 +78,7 @@ mod tests; mod tests_currency_adapter; mod tests_events; mod tests_fungibles; +mod tests_fungibles_conformance; mod tests_multicurrency; mod weights; diff --git a/tokens/src/tests_fungibles_conformance.rs b/tokens/src/tests_fungibles_conformance.rs new file mode 100644 index 000000000..090455270 --- /dev/null +++ b/tokens/src/tests_fungibles_conformance.rs @@ -0,0 +1,75 @@ +#![cfg(test)] + +use super::*; +use crate::fungibles_conformance_tests; +use mock::*; +use paste::paste; + +macro_rules! run_tests { + ($path:path, $currency_id:expr, $($name:ident),*) => { + $( + paste! { + #[test] + fn [< $name _ $currency_id _dust_trap_on >]() { + let trap_account = DustReceiverAccount::get(); + let builder = ExtBuilder::default(); + builder.build().execute_with(|| { + >::set_balance($currency_id, &trap_account, Tokens::minimum_balance($currency_id)); + $path::$name::< + Tokens, + ::AccountId, + >($currency_id, Some(trap_account)); + }); + } + + #[test] + fn [< $name _ $currency_id _dust_trap_off >]() { + let trap_account = DustReceiverAccount::get(); + let builder = ExtBuilder::default(); + builder.build().execute_with(|| { + GetDustReceiverAccount::set(None); + $path::$name::< + Tokens, + ::AccountId, + >($currency_id, None); + }); + } + } + )* + }; + ($path:path, $currency_id:expr) => { + run_tests!( + $path, + $currency_id, + mint_into_success, + mint_into_overflow, + mint_into_below_minimum, + burn_from_exact_success, + burn_from_best_effort_success, + burn_from_exact_insufficient_funds, + restore_success, + restore_overflow, + restore_below_minimum, + shelve_success, + shelve_insufficient_funds, + transfer_success, + transfer_expendable_all, + transfer_expendable_dust, + transfer_protect_preserve, + set_balance_mint_success, + set_balance_burn_success, + can_deposit_success, + can_deposit_below_minimum, + can_deposit_overflow, + can_withdraw_success, + can_withdraw_reduced_to_zero, + can_withdraw_balance_low, + reducible_balance_expendable, + reducible_balance_protect_preserve + ); + }; +} + +run_tests!(fungibles_conformance_tests::inspect_mutate, DOT); +run_tests!(fungibles_conformance_tests::inspect_mutate, BTC); +run_tests!(fungibles_conformance_tests::inspect_mutate, ETH);