mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-14 01:41:09 +00:00
Deprecate Currency; introduce holds and freezing into fungible traits (#12951)
* First reworking of fungibles API * New API and docs * More fungible::* API improvements * New ref-counting logic for old API * Missing files * Fixes * Use the new transfer logic * Use fungibles for the dispatchables * Use shelve/restore names * Locking works with total balance. * repotting and removal * Separate Holds from Reserves * Introduce freezes * Missing files * Tests for freezing * Fix hold+freeze combo * More tests * Fee-free dispatchable for upgrading accounts * Benchmarks and a few fixes * Another test * Docs and refactor to avoid blanket impls * Repot * Fit out ItemOf fully * Add events to Balanced traits * Introduced events into Hold traits * Fix Assets pallet tests * Assets benchmarks pass * Missing files and fixes * Fixes * Fixes * Benchmarks fixes * Fix balance benchmarks * Formatting * Expose fungible sub modules * Move NIS to fungible API * Fix broken impl and add test * Fix tests * API for `transfer_and_hold` * Use composite APIs * Formatting * Upgraded event * Fixes * Fixes * Fixes * Fixes * Repot tests and some fixed * Fix some bits * Fix dust tests * Rename `set_balance` - `Balances::set_balance` becomes `Balances::force_set_balance` - `Unbalanced::set_balance` becomes `Unbalances::write_balance` * becomes * Move dust handling to fungibles API * Formatting * Fixes and more refactoring * Fixes * Fixes * Fixes * Fixes * Fixes * Fixes * Fixes * Fixes * Fixes * Use reducible_balance for better correctness on fees * Reducing hold to zero should remove entry. * Add test * Docs * Update frame/support/src/traits/tokens/fungibles/hold.rs Co-authored-by: Muharem Ismailov <ismailov.m.h@gmail.com> * Update frame/support/src/traits/tokens/fungibles/regular.rs Co-authored-by: Muharem Ismailov <ismailov.m.h@gmail.com> * Update frame/support/src/traits/tokens/fungible/hold.rs Co-authored-by: Muharem Ismailov <ismailov.m.h@gmail.com> * Update frame/support/src/traits/tokens/fungible/regular.rs Co-authored-by: Muharem Ismailov <ismailov.m.h@gmail.com> * Docs * Docs * Docs * Fix NIS benchmarks * Doc comment * Remove post_mutation * Fix some tests * Fix some grumbles * Enumify bool args to fungible(s) functions * Fix up assets and balances * Formatting * Fix contracts * Fix tests & benchmarks build * Typify minted boolean arg * Typify on_hold boolean arg; renames * Fix numerous tests * Fix dependency issue * Privatize dangerous API mutate_account * Fix contracts (@alext - please check this commit) * Remove println * Fix tests for contracts * Fix broken rename * Fix broken rename * Fix broken rename * Docs * Update frame/support/src/traits/tokens/fungible/hold.rs Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> * remove from_ref_time * Update frame/executive/src/lib.rs Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> * Update frame/executive/src/lib.rs Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> * Reenable test * Update frame/support/src/traits/tokens/fungibles/hold.rs Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> * Update frame/support/src/traits/tokens/fungible/hold.rs Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> * Update frame/support/src/traits/tokens/fungible/hold.rs Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> * Update frame/support/src/traits/tokens/fungible/hold.rs Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> * Update frame/support/src/traits/tokens/currency.rs Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> * Update frame/lottery/src/tests.rs Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> * Update frame/support/src/traits/tokens/fungible/mod.rs Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> * Update frame/support/src/traits/tokens/fungible/regular.rs Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> * Update frame/support/src/traits/tokens/fungibles/freeze.rs Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> * Update frame/support/src/traits/tokens/fungible/regular.rs Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> * Update frame/support/src/traits/tokens/fungibles/hold.rs Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> * Update frame/support/src/traits/tokens/fungibles/hold.rs Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> * Update frame/support/src/traits/tokens/fungibles/hold.rs Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> * Rename UnwantedRemoval to UnwantedAccountRemoval * Docs * Formatting * Update frame/balances/src/lib.rs Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Update primitives/runtime/src/lib.rs Co-authored-by: Keith Yeung <kungfukeith11@gmail.com> * handle_raw_dust oes nothing * Formatting * Fixes * Grumble * Fixes * Add test * Add test * Tests for reducible_balance * Fixes * Fix Salary * Fixes * Disable broken test * Disable nicely * Fixes * Fixes * Fixes * Rename some events * Fix nomination pools breakage * Add compatibility stub for transfer tx * Reinstate a safely compatible version of Balances set_balance * Fixes * Grumble * Update frame/nis/src/lib.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * ".git/.scripts/commands/bench/bench.sh" pallet dev pallet_balances * disable flakey tests * Update frame/balances/src/lib.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * Grumbles * Grumble --------- Co-authored-by: Muharem Ismailov <ismailov.m.h@gmail.com> Co-authored-by: Alexander Theißen <alex.theissen@me.com> Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com> Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> Co-authored-by: Keith Yeung <kungfukeith11@gmail.com> Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Co-authored-by: command-bot <>
This commit is contained in:
@@ -24,6 +24,8 @@ use crate::Pallet as Balances;
|
||||
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_system::RawOrigin;
|
||||
use sp_runtime::traits::Bounded;
|
||||
use types::ExtraFlags;
|
||||
|
||||
const SEED: u32 = 0;
|
||||
// existential deposit multiplier
|
||||
@@ -37,7 +39,7 @@ mod benchmarks {
|
||||
// * Transfer will kill the sender account.
|
||||
// * Transfer will create the recipient account.
|
||||
#[benchmark]
|
||||
fn transfer() {
|
||||
fn transfer_allow_death() {
|
||||
let existential_deposit = T::ExistentialDeposit::get();
|
||||
let caller = whitelisted_caller();
|
||||
|
||||
@@ -79,7 +81,7 @@ mod benchmarks {
|
||||
let transfer_amount = existential_deposit.saturating_mul(ED_MULTIPLIER.into());
|
||||
|
||||
#[extrinsic_call]
|
||||
transfer(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount);
|
||||
transfer_allow_death(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount);
|
||||
|
||||
assert!(!Balances::<T, I>::free_balance(&caller).is_zero());
|
||||
assert!(!Balances::<T, I>::free_balance(&recipient).is_zero());
|
||||
@@ -106,9 +108,9 @@ mod benchmarks {
|
||||
assert_eq!(Balances::<T, I>::free_balance(&recipient), transfer_amount);
|
||||
}
|
||||
|
||||
// Benchmark `set_balance` coming from ROOT account. This always creates an account.
|
||||
// Benchmark `force_set_balance` coming from ROOT account. This always creates an account.
|
||||
#[benchmark]
|
||||
fn set_balance_creating() {
|
||||
fn force_set_balance_creating() {
|
||||
let user: T::AccountId = account("user", 0, SEED);
|
||||
let user_lookup = T::Lookup::unlookup(user.clone());
|
||||
|
||||
@@ -118,15 +120,14 @@ mod benchmarks {
|
||||
let _ = <Balances<T, I> as Currency<_>>::make_free_balance_be(&user, balance_amount);
|
||||
|
||||
#[extrinsic_call]
|
||||
set_balance(RawOrigin::Root, user_lookup, balance_amount, balance_amount);
|
||||
force_set_balance(RawOrigin::Root, user_lookup, balance_amount);
|
||||
|
||||
assert_eq!(Balances::<T, I>::free_balance(&user), balance_amount);
|
||||
assert_eq!(Balances::<T, I>::reserved_balance(&user), balance_amount);
|
||||
}
|
||||
|
||||
// Benchmark `set_balance` coming from ROOT account. This always kills an account.
|
||||
// Benchmark `force_set_balance` coming from ROOT account. This always kills an account.
|
||||
#[benchmark]
|
||||
fn set_balance_killing() {
|
||||
fn force_set_balance_killing() {
|
||||
let user: T::AccountId = account("user", 0, SEED);
|
||||
let user_lookup = T::Lookup::unlookup(user.clone());
|
||||
|
||||
@@ -136,7 +137,7 @@ mod benchmarks {
|
||||
let _ = <Balances<T, I> as Currency<_>>::make_free_balance_be(&user, balance_amount);
|
||||
|
||||
#[extrinsic_call]
|
||||
set_balance(RawOrigin::Root, user_lookup, Zero::zero(), Zero::zero());
|
||||
force_set_balance(RawOrigin::Root, user_lookup, Zero::zero());
|
||||
|
||||
assert!(Balances::<T, I>::free_balance(&user).is_zero());
|
||||
}
|
||||
@@ -197,7 +198,7 @@ mod benchmarks {
|
||||
}
|
||||
|
||||
#[extrinsic_call]
|
||||
transfer(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount);
|
||||
transfer_allow_death(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount);
|
||||
|
||||
assert_eq!(Balances::<T, I>::free_balance(&caller), Zero::zero());
|
||||
assert_eq!(Balances::<T, I>::free_balance(&recipient), transfer_amount);
|
||||
@@ -230,27 +231,64 @@ mod benchmarks {
|
||||
let user_lookup = T::Lookup::unlookup(user.clone());
|
||||
|
||||
// Give some multiple of the existential deposit
|
||||
let existential_deposit = T::ExistentialDeposit::get();
|
||||
let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into());
|
||||
let ed = T::ExistentialDeposit::get();
|
||||
let balance = ed + ed;
|
||||
let _ = <Balances<T, I> as Currency<_>>::make_free_balance_be(&user, balance);
|
||||
|
||||
// Reserve the balance
|
||||
<Balances<T, I> as ReservableCurrency<_>>::reserve(&user, balance)?;
|
||||
assert_eq!(Balances::<T, I>::reserved_balance(&user), balance);
|
||||
assert!(Balances::<T, I>::free_balance(&user).is_zero());
|
||||
<Balances<T, I> as ReservableCurrency<_>>::reserve(&user, ed)?;
|
||||
assert_eq!(Balances::<T, I>::reserved_balance(&user), ed);
|
||||
assert_eq!(Balances::<T, I>::free_balance(&user), ed);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, user_lookup, balance);
|
||||
|
||||
assert!(Balances::<T, I>::reserved_balance(&user).is_zero());
|
||||
assert_eq!(Balances::<T, I>::free_balance(&user), balance);
|
||||
assert_eq!(Balances::<T, I>::free_balance(&user), ed + ed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn upgrade_accounts(u: Linear<1, 1_000>) {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let who = (0..u)
|
||||
.into_iter()
|
||||
.map(|i| -> T::AccountId {
|
||||
let user = account("old_user", i, SEED);
|
||||
let account = AccountData {
|
||||
free: T::ExistentialDeposit::get(),
|
||||
reserved: T::ExistentialDeposit::get(),
|
||||
frozen: Zero::zero(),
|
||||
flags: ExtraFlags::old_logic(),
|
||||
};
|
||||
frame_system::Pallet::<T>::inc_providers(&user);
|
||||
assert!(T::AccountStore::try_mutate_exists(&user, |a| -> DispatchResult {
|
||||
*a = Some(account);
|
||||
Ok(())
|
||||
})
|
||||
.is_ok());
|
||||
assert!(!Balances::<T, I>::account(&user).flags.is_new_logic());
|
||||
assert_eq!(frame_system::Pallet::<T>::providers(&user), 1);
|
||||
assert_eq!(frame_system::Pallet::<T>::consumers(&user), 0);
|
||||
user
|
||||
})
|
||||
.collect();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()), who);
|
||||
|
||||
for i in 0..u {
|
||||
let user: T::AccountId = account("old_user", i, SEED);
|
||||
assert!(Balances::<T, I>::account(&user).flags.is_new_logic());
|
||||
assert_eq!(frame_system::Pallet::<T>::providers(&user), 1);
|
||||
assert_eq!(frame_system::Pallet::<T>::consumers(&user), 1);
|
||||
}
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite! {
|
||||
Balances,
|
||||
crate::tests_composite::ExtBuilder::default().build(),
|
||||
crate::tests_composite::Test,
|
||||
crate::tests::ExtBuilder::default().build(),
|
||||
crate::tests::Test,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,898 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Implementations for the `Currency` family of traits.
|
||||
|
||||
use super::*;
|
||||
use frame_support::{
|
||||
ensure,
|
||||
pallet_prelude::DispatchResult,
|
||||
traits::{
|
||||
tokens::{fungible, BalanceStatus as Status},
|
||||
Currency, DefensiveSaturating, ExistenceRequirement,
|
||||
ExistenceRequirement::AllowDeath,
|
||||
Get, Imbalance, LockIdentifier, LockableCurrency, NamedReservableCurrency,
|
||||
ReservableCurrency, SignedImbalance, TryDrop, WithdrawReasons,
|
||||
},
|
||||
};
|
||||
pub use imbalances::{NegativeImbalance, PositiveImbalance};
|
||||
|
||||
// wrapping these imbalances in a private module is necessary to ensure absolute privacy
|
||||
// of the inner member.
|
||||
mod imbalances {
|
||||
use super::{result, Config, Imbalance, RuntimeDebug, Saturating, TryDrop, Zero};
|
||||
use frame_support::traits::SameOrOther;
|
||||
use sp_std::mem;
|
||||
|
||||
/// 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]
|
||||
#[derive(RuntimeDebug, PartialEq, Eq)]
|
||||
pub struct PositiveImbalance<T: Config<I>, I: 'static = ()>(T::Balance);
|
||||
|
||||
impl<T: Config<I>, I: 'static> PositiveImbalance<T, I> {
|
||||
/// Create a new positive imbalance from a balance.
|
||||
pub fn new(amount: T::Balance) -> Self {
|
||||
PositiveImbalance(amount)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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]
|
||||
#[derive(RuntimeDebug, PartialEq, Eq)]
|
||||
pub struct NegativeImbalance<T: Config<I>, I: 'static = ()>(T::Balance);
|
||||
|
||||
impl<T: Config<I>, I: 'static> NegativeImbalance<T, I> {
|
||||
/// Create a new negative imbalance from a balance.
|
||||
pub fn new(amount: T::Balance) -> Self {
|
||||
NegativeImbalance(amount)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> TryDrop for PositiveImbalance<T, I> {
|
||||
fn try_drop(self) -> result::Result<(), Self> {
|
||||
self.drop_zero()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> Default for PositiveImbalance<T, I> {
|
||||
fn default() -> Self {
|
||||
Self::zero()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> Imbalance<T::Balance> for PositiveImbalance<T, I> {
|
||||
type Opposite = NegativeImbalance<T, I>;
|
||||
|
||||
fn zero() -> Self {
|
||||
Self(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 - first;
|
||||
|
||||
mem::forget(self);
|
||||
(Self(first), Self(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);
|
||||
}
|
||||
fn offset(self, other: Self::Opposite) -> SameOrOther<Self, Self::Opposite> {
|
||||
let (a, b) = (self.0, other.0);
|
||||
mem::forget((self, other));
|
||||
|
||||
if a > b {
|
||||
SameOrOther::Same(Self(a - b))
|
||||
} else if b > a {
|
||||
SameOrOther::Other(NegativeImbalance::new(b - a))
|
||||
} else {
|
||||
SameOrOther::None
|
||||
}
|
||||
}
|
||||
fn peek(&self) -> T::Balance {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> TryDrop for NegativeImbalance<T, I> {
|
||||
fn try_drop(self) -> result::Result<(), Self> {
|
||||
self.drop_zero()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> Default for NegativeImbalance<T, I> {
|
||||
fn default() -> Self {
|
||||
Self::zero()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> Imbalance<T::Balance> for NegativeImbalance<T, I> {
|
||||
type Opposite = PositiveImbalance<T, I>;
|
||||
|
||||
fn zero() -> Self {
|
||||
Self(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 - first;
|
||||
|
||||
mem::forget(self);
|
||||
(Self(first), Self(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);
|
||||
}
|
||||
fn offset(self, other: Self::Opposite) -> SameOrOther<Self, Self::Opposite> {
|
||||
let (a, b) = (self.0, other.0);
|
||||
mem::forget((self, other));
|
||||
|
||||
if a > b {
|
||||
SameOrOther::Same(Self(a - b))
|
||||
} else if b > a {
|
||||
SameOrOther::Other(PositiveImbalance::new(b - a))
|
||||
} else {
|
||||
SameOrOther::None
|
||||
}
|
||||
}
|
||||
fn peek(&self) -> T::Balance {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> Drop for PositiveImbalance<T, I> {
|
||||
/// Basic drop handler will just square up the total issuance.
|
||||
fn drop(&mut self) {
|
||||
<super::TotalIssuance<T, I>>::mutate(|v| *v = v.saturating_add(self.0));
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> Drop for NegativeImbalance<T, I> {
|
||||
/// Basic drop handler will just square up the total issuance.
|
||||
fn drop(&mut self) {
|
||||
<super::TotalIssuance<T, I>>::mutate(|v| *v = v.saturating_sub(self.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> Currency<T::AccountId> for Pallet<T, I>
|
||||
where
|
||||
T::Balance: MaybeSerializeDeserialize + Debug,
|
||||
{
|
||||
type Balance = T::Balance;
|
||||
type PositiveImbalance = PositiveImbalance<T, I>;
|
||||
type NegativeImbalance = NegativeImbalance<T, I>;
|
||||
|
||||
fn total_balance(who: &T::AccountId) -> Self::Balance {
|
||||
Self::account(who).total()
|
||||
}
|
||||
|
||||
// Check if `value` amount of free balance can be slashed from `who`.
|
||||
fn can_slash(who: &T::AccountId, value: Self::Balance) -> bool {
|
||||
if value.is_zero() {
|
||||
return true
|
||||
}
|
||||
Self::free_balance(who) >= value
|
||||
}
|
||||
|
||||
fn total_issuance() -> Self::Balance {
|
||||
TotalIssuance::<T, I>::get()
|
||||
}
|
||||
|
||||
fn active_issuance() -> Self::Balance {
|
||||
<Self as fungible::Inspect<_>>::active_issuance()
|
||||
}
|
||||
|
||||
fn deactivate(amount: Self::Balance) {
|
||||
<Self as fungible::Unbalanced<_>>::deactivate(amount);
|
||||
}
|
||||
|
||||
fn reactivate(amount: Self::Balance) {
|
||||
<Self as fungible::Unbalanced<_>>::reactivate(amount);
|
||||
}
|
||||
|
||||
fn minimum_balance() -> Self::Balance {
|
||||
T::ExistentialDeposit::get()
|
||||
}
|
||||
|
||||
// Burn funds from the total issuance, returning a positive imbalance for the amount burned.
|
||||
// Is a no-op if amount to be burned is zero.
|
||||
fn burn(mut amount: Self::Balance) -> Self::PositiveImbalance {
|
||||
if amount.is_zero() {
|
||||
return PositiveImbalance::zero()
|
||||
}
|
||||
<TotalIssuance<T, I>>::mutate(|issued| {
|
||||
*issued = issued.checked_sub(&amount).unwrap_or_else(|| {
|
||||
amount = *issued;
|
||||
Zero::zero()
|
||||
});
|
||||
});
|
||||
PositiveImbalance::new(amount)
|
||||
}
|
||||
|
||||
// Create new funds into the total issuance, returning a negative imbalance
|
||||
// for the amount issued.
|
||||
// Is a no-op if amount to be issued it zero.
|
||||
fn issue(mut amount: Self::Balance) -> Self::NegativeImbalance {
|
||||
if amount.is_zero() {
|
||||
return NegativeImbalance::zero()
|
||||
}
|
||||
<TotalIssuance<T, I>>::mutate(|issued| {
|
||||
*issued = issued.checked_add(&amount).unwrap_or_else(|| {
|
||||
amount = Self::Balance::max_value() - *issued;
|
||||
Self::Balance::max_value()
|
||||
})
|
||||
});
|
||||
NegativeImbalance::new(amount)
|
||||
}
|
||||
|
||||
fn free_balance(who: &T::AccountId) -> Self::Balance {
|
||||
Self::account(who).free
|
||||
}
|
||||
|
||||
// Ensure that an account can withdraw from their free balance given any existing withdrawal
|
||||
// restrictions like locks and vesting balance.
|
||||
// Is a no-op if amount to be withdrawn is zero.
|
||||
fn ensure_can_withdraw(
|
||||
who: &T::AccountId,
|
||||
amount: T::Balance,
|
||||
_reasons: WithdrawReasons,
|
||||
new_balance: T::Balance,
|
||||
) -> DispatchResult {
|
||||
if amount.is_zero() {
|
||||
return Ok(())
|
||||
}
|
||||
ensure!(new_balance >= Self::account(who).frozen, Error::<T, I>::LiquidityRestrictions);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Transfer some free balance from `transactor` to `dest`, respecting existence requirements.
|
||||
// Is a no-op if value to be transferred is zero or the `transactor` is the same as `dest`.
|
||||
fn transfer(
|
||||
transactor: &T::AccountId,
|
||||
dest: &T::AccountId,
|
||||
value: Self::Balance,
|
||||
existence_requirement: ExistenceRequirement,
|
||||
) -> DispatchResult {
|
||||
if value.is_zero() || transactor == dest {
|
||||
return Ok(())
|
||||
}
|
||||
let keep_alive = match existence_requirement {
|
||||
ExistenceRequirement::KeepAlive => Preserve,
|
||||
ExistenceRequirement::AllowDeath => Expendable,
|
||||
};
|
||||
<Self as fungible::Mutate<_>>::transfer(transactor, dest, value, keep_alive)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Slash a target account `who`, returning the negative imbalance created and any left over
|
||||
/// amount that could not be slashed.
|
||||
///
|
||||
/// Is a no-op if `value` to be slashed is zero or the account does not exist.
|
||||
///
|
||||
/// 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(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) {
|
||||
if value.is_zero() {
|
||||
return (NegativeImbalance::zero(), Zero::zero())
|
||||
}
|
||||
if Self::total_balance(who).is_zero() {
|
||||
return (NegativeImbalance::zero(), value)
|
||||
}
|
||||
|
||||
let result = match Self::try_mutate_account_handling_dust(
|
||||
who,
|
||||
|account, _is_new| -> Result<(Self::NegativeImbalance, Self::Balance), DispatchError> {
|
||||
// Best value is the most amount we can slash following liveness rules.
|
||||
let ed = T::ExistentialDeposit::get();
|
||||
let actual = match system::Pallet::<T>::can_dec_provider(who) {
|
||||
true => value.min(account.free),
|
||||
false => value.min(account.free.saturating_sub(ed)),
|
||||
};
|
||||
account.free.saturating_reduce(actual);
|
||||
let remaining = value.saturating_sub(actual);
|
||||
Ok((NegativeImbalance::new(actual), remaining))
|
||||
},
|
||||
) {
|
||||
Ok((imbalance, remaining)) => {
|
||||
Self::deposit_event(Event::Slashed {
|
||||
who: who.clone(),
|
||||
amount: value.saturating_sub(remaining),
|
||||
});
|
||||
(imbalance, remaining)
|
||||
},
|
||||
Err(_) => (Self::NegativeImbalance::zero(), value),
|
||||
};
|
||||
result
|
||||
}
|
||||
|
||||
/// Deposit some `value` into the free balance of an existing target account `who`.
|
||||
///
|
||||
/// Is a no-op if the `value` to be deposited is zero.
|
||||
fn deposit_into_existing(
|
||||
who: &T::AccountId,
|
||||
value: Self::Balance,
|
||||
) -> Result<Self::PositiveImbalance, DispatchError> {
|
||||
if value.is_zero() {
|
||||
return Ok(PositiveImbalance::zero())
|
||||
}
|
||||
|
||||
Self::try_mutate_account_handling_dust(
|
||||
who,
|
||||
|account, is_new| -> Result<Self::PositiveImbalance, DispatchError> {
|
||||
ensure!(!is_new, Error::<T, I>::DeadAccount);
|
||||
account.free = account.free.checked_add(&value).ok_or(ArithmeticError::Overflow)?;
|
||||
Self::deposit_event(Event::Deposit { who: who.clone(), amount: value });
|
||||
Ok(PositiveImbalance::new(value))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Deposit some `value` into the free balance of `who`, possibly creating a new account.
|
||||
///
|
||||
/// This function is a no-op if:
|
||||
/// - the `value` to be deposited is zero; or
|
||||
/// - the `value` to be deposited is less than the required ED and the account does not yet
|
||||
/// exist; or
|
||||
/// - the deposit would necessitate the account to exist and there are no provider references;
|
||||
/// or
|
||||
/// - `value` is so large it would cause the balance of `who` to overflow.
|
||||
fn deposit_creating(who: &T::AccountId, value: Self::Balance) -> Self::PositiveImbalance {
|
||||
if value.is_zero() {
|
||||
return Self::PositiveImbalance::zero()
|
||||
}
|
||||
|
||||
Self::try_mutate_account_handling_dust(
|
||||
who,
|
||||
|account, is_new| -> Result<Self::PositiveImbalance, DispatchError> {
|
||||
let ed = T::ExistentialDeposit::get();
|
||||
ensure!(value >= ed || !is_new, Error::<T, I>::ExistentialDeposit);
|
||||
|
||||
// defensive only: overflow should never happen, however in case it does, then this
|
||||
// operation is a no-op.
|
||||
account.free = match account.free.checked_add(&value) {
|
||||
Some(x) => x,
|
||||
None => return Ok(Self::PositiveImbalance::zero()),
|
||||
};
|
||||
|
||||
Self::deposit_event(Event::Deposit { who: who.clone(), amount: value });
|
||||
Ok(PositiveImbalance::new(value))
|
||||
},
|
||||
)
|
||||
.unwrap_or_else(|_| Self::PositiveImbalance::zero())
|
||||
}
|
||||
|
||||
/// Withdraw some free balance from an account, respecting existence requirements.
|
||||
///
|
||||
/// Is a no-op if value to be withdrawn is zero.
|
||||
fn withdraw(
|
||||
who: &T::AccountId,
|
||||
value: Self::Balance,
|
||||
reasons: WithdrawReasons,
|
||||
liveness: ExistenceRequirement,
|
||||
) -> result::Result<Self::NegativeImbalance, DispatchError> {
|
||||
if value.is_zero() {
|
||||
return Ok(NegativeImbalance::zero())
|
||||
}
|
||||
|
||||
Self::try_mutate_account_handling_dust(
|
||||
who,
|
||||
|account, _| -> Result<Self::NegativeImbalance, DispatchError> {
|
||||
let new_free_account =
|
||||
account.free.checked_sub(&value).ok_or(Error::<T, I>::InsufficientBalance)?;
|
||||
|
||||
// bail if we need to keep the account alive and this would kill it.
|
||||
let ed = T::ExistentialDeposit::get();
|
||||
let would_be_dead = new_free_account < ed;
|
||||
let would_kill = would_be_dead && account.free >= ed;
|
||||
ensure!(liveness == AllowDeath || !would_kill, Error::<T, I>::Expendability);
|
||||
|
||||
Self::ensure_can_withdraw(who, value, reasons, new_free_account)?;
|
||||
|
||||
account.free = new_free_account;
|
||||
|
||||
Self::deposit_event(Event::Withdraw { who: who.clone(), amount: value });
|
||||
Ok(NegativeImbalance::new(value))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Force the new free balance of a target account `who` to some new value `balance`.
|
||||
fn make_free_balance_be(
|
||||
who: &T::AccountId,
|
||||
value: Self::Balance,
|
||||
) -> SignedImbalance<Self::Balance, Self::PositiveImbalance> {
|
||||
Self::try_mutate_account_handling_dust(
|
||||
who,
|
||||
|account,
|
||||
is_new|
|
||||
-> Result<SignedImbalance<Self::Balance, Self::PositiveImbalance>, DispatchError> {
|
||||
let ed = T::ExistentialDeposit::get();
|
||||
// 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.
|
||||
ensure!(value >= ed || !is_new, Error::<T, I>::ExistentialDeposit);
|
||||
|
||||
let imbalance = if account.free <= value {
|
||||
SignedImbalance::Positive(PositiveImbalance::new(value - account.free))
|
||||
} else {
|
||||
SignedImbalance::Negative(NegativeImbalance::new(account.free - value))
|
||||
};
|
||||
account.free = value;
|
||||
Self::deposit_event(Event::BalanceSet { who: who.clone(), free: account.free });
|
||||
Ok(imbalance)
|
||||
},
|
||||
)
|
||||
.unwrap_or_else(|_| SignedImbalance::Positive(Self::PositiveImbalance::zero()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> ReservableCurrency<T::AccountId> for Pallet<T, I>
|
||||
where
|
||||
T::Balance: MaybeSerializeDeserialize + Debug,
|
||||
{
|
||||
/// Check if `who` can reserve `value` from their free balance.
|
||||
///
|
||||
/// Always `true` if value to be reserved is zero.
|
||||
fn can_reserve(who: &T::AccountId, value: Self::Balance) -> bool {
|
||||
if value.is_zero() {
|
||||
return true
|
||||
}
|
||||
Self::account(who).free.checked_sub(&value).map_or(false, |new_balance| {
|
||||
Self::ensure_can_withdraw(who, value, WithdrawReasons::RESERVE, new_balance).is_ok()
|
||||
})
|
||||
}
|
||||
|
||||
fn reserved_balance(who: &T::AccountId) -> Self::Balance {
|
||||
Self::account(who).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(who: &T::AccountId, value: Self::Balance) -> DispatchResult {
|
||||
if value.is_zero() {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
Self::try_mutate_account_handling_dust(who, |account, _| -> DispatchResult {
|
||||
account.free =
|
||||
account.free.checked_sub(&value).ok_or(Error::<T, I>::InsufficientBalance)?;
|
||||
account.reserved =
|
||||
account.reserved.checked_add(&value).ok_or(ArithmeticError::Overflow)?;
|
||||
Self::ensure_can_withdraw(&who, value, WithdrawReasons::RESERVE, account.free)
|
||||
})?;
|
||||
|
||||
Self::deposit_event(Event::Reserved { 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 or the account does not exist.
|
||||
///
|
||||
/// NOTE: returns amount value which wasn't successfully unreserved.
|
||||
fn unreserve(who: &T::AccountId, value: Self::Balance) -> Self::Balance {
|
||||
if value.is_zero() {
|
||||
return Zero::zero()
|
||||
}
|
||||
if Self::total_balance(who).is_zero() {
|
||||
return value
|
||||
}
|
||||
|
||||
let actual = match Self::mutate_account_handling_dust(who, |account| {
|
||||
let actual = cmp::min(account.reserved, value);
|
||||
account.reserved -= actual;
|
||||
// defensive only: this can never fail since total issuance which is at least
|
||||
// free+reserved fits into the same data type.
|
||||
account.free = account.free.defensive_saturating_add(actual);
|
||||
actual
|
||||
}) {
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
// This should never happen since we don't alter the total amount in the account.
|
||||
// If it ever does, then we should fail gracefully though, indicating that nothing
|
||||
// could be done.
|
||||
return value
|
||||
},
|
||||
};
|
||||
|
||||
Self::deposit_event(Event::Unreserved { who: who.clone(), amount: actual });
|
||||
value - actual
|
||||
}
|
||||
|
||||
/// Slash from reserved balance, returning the negative imbalance created,
|
||||
/// and any amount that was unable to be slashed.
|
||||
///
|
||||
/// Is a no-op if the value to be slashed is zero or the account does not exist.
|
||||
fn slash_reserved(
|
||||
who: &T::AccountId,
|
||||
value: Self::Balance,
|
||||
) -> (Self::NegativeImbalance, Self::Balance) {
|
||||
if value.is_zero() {
|
||||
return (NegativeImbalance::zero(), Zero::zero())
|
||||
}
|
||||
if Self::total_balance(who).is_zero() {
|
||||
return (NegativeImbalance::zero(), value)
|
||||
}
|
||||
|
||||
// NOTE: `mutate_account` may fail if it attempts to reduce the balance to the point that an
|
||||
// account is attempted to be illegally destroyed.
|
||||
|
||||
match Self::mutate_account_handling_dust(who, |account| {
|
||||
let actual = value.min(account.reserved);
|
||||
account.reserved.saturating_reduce(actual);
|
||||
|
||||
// underflow should never happen, but it if does, there's nothing to be done here.
|
||||
(NegativeImbalance::new(actual), value.saturating_sub(actual))
|
||||
}) {
|
||||
Ok((imbalance, not_slashed)) => {
|
||||
Self::deposit_event(Event::Slashed {
|
||||
who: who.clone(),
|
||||
amount: value.saturating_sub(not_slashed),
|
||||
});
|
||||
(imbalance, not_slashed)
|
||||
},
|
||||
Err(_) => (Self::NegativeImbalance::zero(), value),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
slashed: &T::AccountId,
|
||||
beneficiary: &T::AccountId,
|
||||
value: Self::Balance,
|
||||
status: Status,
|
||||
) -> Result<Self::Balance, DispatchError> {
|
||||
let actual = Self::do_transfer_reserved(slashed, beneficiary, value, true, status)?;
|
||||
Ok(value.saturating_sub(actual))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> NamedReservableCurrency<T::AccountId> for Pallet<T, I>
|
||||
where
|
||||
T::Balance: MaybeSerializeDeserialize + Debug,
|
||||
{
|
||||
type ReserveIdentifier = T::ReserveIdentifier;
|
||||
|
||||
fn reserved_balance_named(id: &Self::ReserveIdentifier, who: &T::AccountId) -> Self::Balance {
|
||||
let reserves = Self::reserves(who);
|
||||
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,
|
||||
who: &T::AccountId,
|
||||
value: Self::Balance,
|
||||
) -> DispatchResult {
|
||||
if value.is_zero() {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
Reserves::<T, I>::try_mutate(who, |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::<T, I>::TooManyReserves)?;
|
||||
},
|
||||
};
|
||||
<Self as ReservableCurrency<_>>::reserve(who, 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_named(
|
||||
id: &Self::ReserveIdentifier,
|
||||
who: &T::AccountId,
|
||||
value: Self::Balance,
|
||||
) -> Self::Balance {
|
||||
if value.is_zero() {
|
||||
return Zero::zero()
|
||||
}
|
||||
|
||||
Reserves::<T, I>::mutate_exists(who, |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 = <Self as ReservableCurrency<_>>::unreserve(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`; qed;
|
||||
reserves[index].amount -= 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 - actual
|
||||
},
|
||||
Err(_) => value,
|
||||
}
|
||||
} else {
|
||||
value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Slash from reserved balance, returning the negative imbalance created,
|
||||
/// and any 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,
|
||||
who: &T::AccountId,
|
||||
value: Self::Balance,
|
||||
) -> (Self::NegativeImbalance, Self::Balance) {
|
||||
if value.is_zero() {
|
||||
return (NegativeImbalance::zero(), Zero::zero())
|
||||
}
|
||||
|
||||
Reserves::<T, I>::mutate(who, |reserves| -> (Self::NegativeImbalance, Self::Balance) {
|
||||
match reserves.binary_search_by_key(id, |data| data.id) {
|
||||
Ok(index) => {
|
||||
let to_change = cmp::min(reserves[index].amount, value);
|
||||
|
||||
let (imb, remain) =
|
||||
<Self as ReservableCurrency<_>>::slash_reserved(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`; qed;
|
||||
reserves[index].amount -= actual;
|
||||
|
||||
Self::deposit_event(Event::Slashed { who: who.clone(), amount: actual });
|
||||
(imb, value - actual)
|
||||
},
|
||||
Err(_) => (NegativeImbalance::zero(), 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,
|
||||
slashed: &T::AccountId,
|
||||
beneficiary: &T::AccountId,
|
||||
value: Self::Balance,
|
||||
status: Status,
|
||||
) -> Result<Self::Balance, DispatchError> {
|
||||
if value.is_zero() {
|
||||
return Ok(Zero::zero())
|
||||
}
|
||||
|
||||
if slashed == beneficiary {
|
||||
return match status {
|
||||
Status::Free => Ok(Self::unreserve_named(id, slashed, value)),
|
||||
Status::Reserved =>
|
||||
Ok(value.saturating_sub(Self::reserved_balance_named(id, slashed))),
|
||||
}
|
||||
}
|
||||
|
||||
Reserves::<T, I>::try_mutate(slashed, |reserves| -> Result<Self::Balance, DispatchError> {
|
||||
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::<T, I>::try_mutate(
|
||||
beneficiary,
|
||||
|reserves| -> Result<T::Balance, DispatchError> {
|
||||
match reserves.binary_search_by_key(id, |data| data.id) {
|
||||
Ok(index) => {
|
||||
let remain =
|
||||
<Self as ReservableCurrency<_>>::repatriate_reserved(
|
||||
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 =
|
||||
<Self as ReservableCurrency<_>>::repatriate_reserved(
|
||||
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::<T, I>::TooManyReserves)?;
|
||||
|
||||
Ok(actual)
|
||||
},
|
||||
}
|
||||
},
|
||||
)?
|
||||
} else {
|
||||
let remain = <Self as ReservableCurrency<_>>::repatriate_reserved(
|
||||
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`; qed;
|
||||
reserves[index].amount -= actual;
|
||||
|
||||
Ok(value - actual)
|
||||
},
|
||||
Err(_) => Ok(value),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> LockableCurrency<T::AccountId> for Pallet<T, I>
|
||||
where
|
||||
T::Balance: MaybeSerializeDeserialize + Debug,
|
||||
{
|
||||
type Moment = T::BlockNumber;
|
||||
|
||||
type MaxLocks = T::MaxLocks;
|
||||
|
||||
// Set a lock on the balance of `who`.
|
||||
// Is a no-op if lock amount is zero or `reasons` `is_none()`.
|
||||
fn set_lock(
|
||||
id: LockIdentifier,
|
||||
who: &T::AccountId,
|
||||
amount: T::Balance,
|
||||
reasons: WithdrawReasons,
|
||||
) {
|
||||
if amount.is_zero() || reasons.is_empty() {
|
||||
return
|
||||
}
|
||||
let mut new_lock = Some(BalanceLock { id, amount, reasons: reasons.into() });
|
||||
let mut locks = Self::locks(who)
|
||||
.into_iter()
|
||||
.filter_map(|l| if l.id == id { new_lock.take() } else { Some(l) })
|
||||
.collect::<Vec<_>>();
|
||||
if let Some(lock) = new_lock {
|
||||
locks.push(lock)
|
||||
}
|
||||
Self::update_locks(who, &locks[..]);
|
||||
}
|
||||
|
||||
// Extend a lock on the balance of `who`.
|
||||
// Is a no-op if lock amount is zero or `reasons` `is_none()`.
|
||||
fn extend_lock(
|
||||
id: LockIdentifier,
|
||||
who: &T::AccountId,
|
||||
amount: T::Balance,
|
||||
reasons: WithdrawReasons,
|
||||
) {
|
||||
if amount.is_zero() || reasons.is_empty() {
|
||||
return
|
||||
}
|
||||
let mut new_lock = Some(BalanceLock { id, amount, reasons: reasons.into() });
|
||||
let mut locks = Self::locks(who)
|
||||
.into_iter()
|
||||
.filter_map(|l| {
|
||||
if l.id == id {
|
||||
new_lock.take().map(|nl| BalanceLock {
|
||||
id: l.id,
|
||||
amount: l.amount.max(nl.amount),
|
||||
reasons: l.reasons | nl.reasons,
|
||||
})
|
||||
} else {
|
||||
Some(l)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if let Some(lock) = new_lock {
|
||||
locks.push(lock)
|
||||
}
|
||||
Self::update_locks(who, &locks[..]);
|
||||
}
|
||||
|
||||
fn remove_lock(id: LockIdentifier, who: &T::AccountId) {
|
||||
let mut locks = Self::locks(who);
|
||||
locks.retain(|l| l.id != id);
|
||||
Self::update_locks(who, &locks[..]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Implementation of `fungible` traits for Balances pallet.
|
||||
use super::*;
|
||||
use frame_support::traits::tokens::{
|
||||
Fortitude,
|
||||
Preservation::{self, Preserve, Protect},
|
||||
Provenance::{self, Minted},
|
||||
};
|
||||
|
||||
impl<T: Config<I>, I: 'static> fungible::Inspect<T::AccountId> for Pallet<T, I> {
|
||||
type Balance = T::Balance;
|
||||
|
||||
fn total_issuance() -> Self::Balance {
|
||||
TotalIssuance::<T, I>::get()
|
||||
}
|
||||
fn active_issuance() -> Self::Balance {
|
||||
TotalIssuance::<T, I>::get().saturating_sub(InactiveIssuance::<T, I>::get())
|
||||
}
|
||||
fn minimum_balance() -> Self::Balance {
|
||||
T::ExistentialDeposit::get()
|
||||
}
|
||||
fn total_balance(who: &T::AccountId) -> Self::Balance {
|
||||
Self::account(who).total()
|
||||
}
|
||||
fn balance(who: &T::AccountId) -> Self::Balance {
|
||||
Self::account(who).free
|
||||
}
|
||||
fn reducible_balance(
|
||||
who: &T::AccountId,
|
||||
preservation: Preservation,
|
||||
force: Fortitude,
|
||||
) -> Self::Balance {
|
||||
let a = Self::account(who);
|
||||
let mut untouchable = Zero::zero();
|
||||
if force == 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 == Preserve
|
||||
// ..or we don't want the account to die and our provider ref is needed for it to live..
|
||||
|| preservation == Protect && !a.free.is_zero() &&
|
||||
frame_system::Pallet::<T>::providers(who) == 1
|
||||
// ..or we don't care about the account dying but our provider ref is required..
|
||||
|| preservation == Expendable && !a.free.is_zero() &&
|
||||
!frame_system::Pallet::<T>::can_dec_provider(who)
|
||||
{
|
||||
// ..then the ED needed..
|
||||
untouchable = untouchable.max(T::ExistentialDeposit::get());
|
||||
}
|
||||
// Liquid balance is what is neither on hold nor frozen/required for provider.
|
||||
a.free.saturating_sub(untouchable)
|
||||
}
|
||||
fn can_deposit(
|
||||
who: &T::AccountId,
|
||||
amount: Self::Balance,
|
||||
provenance: Provenance,
|
||||
) -> DepositConsequence {
|
||||
if amount.is_zero() {
|
||||
return DepositConsequence::Success
|
||||
}
|
||||
|
||||
if provenance == Minted && TotalIssuance::<T, I>::get().checked_add(&amount).is_none() {
|
||||
return DepositConsequence::Overflow
|
||||
}
|
||||
|
||||
let account = Self::account(who);
|
||||
let new_free = match account.free.checked_add(&amount) {
|
||||
None => return DepositConsequence::Overflow,
|
||||
Some(x) if x < T::ExistentialDeposit::get() => return DepositConsequence::BelowMinimum,
|
||||
Some(x) => x,
|
||||
};
|
||||
|
||||
match account.reserved.checked_add(&new_free) {
|
||||
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(
|
||||
who: &T::AccountId,
|
||||
amount: Self::Balance,
|
||||
) -> WithdrawConsequence<Self::Balance> {
|
||||
if amount.is_zero() {
|
||||
return WithdrawConsequence::Success
|
||||
}
|
||||
|
||||
if TotalIssuance::<T, I>::get().checked_sub(&amount).is_none() {
|
||||
return WithdrawConsequence::Underflow
|
||||
}
|
||||
|
||||
let account = Self::account(who);
|
||||
let new_free_balance = match account.free.checked_sub(&amount) {
|
||||
Some(x) => x,
|
||||
None => return WithdrawConsequence::BalanceLow,
|
||||
};
|
||||
|
||||
let liquid = Self::reducible_balance(who, Expendable, 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 = T::ExistentialDeposit::get();
|
||||
let success = if new_free_balance < ed {
|
||||
if frame_system::Pallet::<T>::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 free funds must be no less than the frozen balance.
|
||||
if new_total_balance < account.frozen {
|
||||
return WithdrawConsequence::Frozen
|
||||
}
|
||||
|
||||
success
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> fungible::Unbalanced<T::AccountId> for Pallet<T, I> {
|
||||
fn handle_dust(dust: fungible::Dust<T::AccountId, Self>) {
|
||||
T::DustRemoval::on_unbalanced(dust.into_credit());
|
||||
}
|
||||
fn write_balance(
|
||||
who: &T::AccountId,
|
||||
amount: Self::Balance,
|
||||
) -> Result<Option<Self::Balance>, DispatchError> {
|
||||
let max_reduction =
|
||||
<Self as fungible::Inspect<_>>::reducible_balance(who, Expendable, Force);
|
||||
let (result, maybe_dust) = Self::mutate_account(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::<T, I>::InsufficientBalance);
|
||||
|
||||
account.free = amount;
|
||||
Ok(())
|
||||
})?;
|
||||
result?;
|
||||
Ok(maybe_dust)
|
||||
}
|
||||
|
||||
fn set_total_issuance(amount: Self::Balance) {
|
||||
TotalIssuance::<T, I>::mutate(|t| *t = amount);
|
||||
}
|
||||
|
||||
fn deactivate(amount: Self::Balance) {
|
||||
InactiveIssuance::<T, I>::mutate(|b| b.saturating_accrue(amount));
|
||||
}
|
||||
|
||||
fn reactivate(amount: Self::Balance) {
|
||||
InactiveIssuance::<T, I>::mutate(|b| b.saturating_reduce(amount));
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> fungible::Mutate<T::AccountId> for Pallet<T, I> {
|
||||
fn done_mint_into(who: &T::AccountId, amount: Self::Balance) {
|
||||
Self::deposit_event(Event::<T, I>::Minted { who: who.clone(), amount });
|
||||
}
|
||||
fn done_burn_from(who: &T::AccountId, amount: Self::Balance) {
|
||||
Self::deposit_event(Event::<T, I>::Burned { who: who.clone(), amount });
|
||||
}
|
||||
fn done_shelve(who: &T::AccountId, amount: Self::Balance) {
|
||||
Self::deposit_event(Event::<T, I>::Suspended { who: who.clone(), amount });
|
||||
}
|
||||
fn done_restore(who: &T::AccountId, amount: Self::Balance) {
|
||||
Self::deposit_event(Event::<T, I>::Restored { who: who.clone(), amount });
|
||||
}
|
||||
fn done_transfer(source: &T::AccountId, dest: &T::AccountId, amount: Self::Balance) {
|
||||
Self::deposit_event(Event::<T, I>::Transfer {
|
||||
from: source.clone(),
|
||||
to: dest.clone(),
|
||||
amount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> fungible::MutateHold<T::AccountId> for Pallet<T, I> {}
|
||||
|
||||
impl<T: Config<I>, I: 'static> fungible::InspectHold<T::AccountId> for Pallet<T, I> {
|
||||
type Reason = T::HoldIdentifier;
|
||||
|
||||
fn total_balance_on_hold(who: &T::AccountId) -> T::Balance {
|
||||
Self::account(who).reserved
|
||||
}
|
||||
fn reducible_total_balance_on_hold(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::account(who);
|
||||
let unavailable = if force == 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(reason: &Self::Reason, who: &T::AccountId) -> T::Balance {
|
||||
Holds::<T, I>::get(who)
|
||||
.iter()
|
||||
.find(|x| &x.id == reason)
|
||||
.map_or_else(Zero::zero, |x| x.amount)
|
||||
}
|
||||
fn hold_available(reason: &Self::Reason, who: &T::AccountId) -> bool {
|
||||
if frame_system::Pallet::<T>::providers(who) == 0 {
|
||||
return false
|
||||
}
|
||||
let holds = Holds::<T, I>::get(who);
|
||||
if holds.is_full() && !holds.iter().any(|x| &x.id == reason) {
|
||||
return false
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> fungible::UnbalancedHold<T::AccountId> for Pallet<T, I> {
|
||||
fn set_balance_on_hold(
|
||||
reason: &Self::Reason,
|
||||
who: &T::AccountId,
|
||||
amount: Self::Balance,
|
||||
) -> DispatchResult {
|
||||
let mut new_account = Self::account(who);
|
||||
let mut holds = Holds::<T, I>::get(who);
|
||||
let mut increase = true;
|
||||
let mut delta = amount;
|
||||
|
||||
if let Some(item) = holds.iter_mut().find(|x| &x.id == reason) {
|
||||
delta = item.amount.max(amount) - item.amount.min(amount);
|
||||
increase = amount > item.amount;
|
||||
item.amount = amount;
|
||||
holds.retain(|x| !x.amount.is_zero());
|
||||
} else {
|
||||
if !amount.is_zero() {
|
||||
holds
|
||||
.try_push(IdAmount { id: *reason, amount })
|
||||
.map_err(|_| Error::<T, I>::TooManyHolds)?;
|
||||
}
|
||||
}
|
||||
|
||||
new_account.reserved = if increase {
|
||||
new_account.reserved.checked_add(&delta).ok_or(ArithmeticError::Overflow)?
|
||||
} else {
|
||||
new_account.reserved.checked_sub(&delta).ok_or(ArithmeticError::Underflow)?
|
||||
};
|
||||
|
||||
let (result, maybe_dust) = Self::try_mutate_account(who, |a, _| -> DispatchResult {
|
||||
*a = new_account;
|
||||
Ok(())
|
||||
})?;
|
||||
debug_assert!(
|
||||
maybe_dust.is_none(),
|
||||
"Does not alter main balance; dust only happens when it is altered; qed"
|
||||
);
|
||||
Holds::<T, I>::insert(who, holds);
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> fungible::InspectFreeze<T::AccountId> for Pallet<T, I> {
|
||||
type Id = T::FreezeIdentifier;
|
||||
|
||||
fn balance_frozen(id: &Self::Id, who: &T::AccountId) -> Self::Balance {
|
||||
let locks = Freezes::<T, I>::get(who);
|
||||
locks.into_iter().find(|l| &l.id == id).map_or(Zero::zero(), |l| l.amount)
|
||||
}
|
||||
|
||||
fn can_freeze(id: &Self::Id, who: &T::AccountId) -> bool {
|
||||
let l = Freezes::<T, I>::get(who);
|
||||
!l.is_full() || l.iter().any(|x| &x.id == id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> fungible::MutateFreeze<T::AccountId> for Pallet<T, I> {
|
||||
fn set_freeze(id: &Self::Id, who: &T::AccountId, amount: Self::Balance) -> DispatchResult {
|
||||
if amount.is_zero() {
|
||||
return Self::thaw(id, who)
|
||||
}
|
||||
let mut locks = Freezes::<T, I>::get(who);
|
||||
if let Some(i) = locks.iter_mut().find(|x| &x.id == id) {
|
||||
i.amount = amount;
|
||||
} else {
|
||||
locks
|
||||
.try_push(IdAmount { id: *id, amount })
|
||||
.map_err(|_| Error::<T, I>::TooManyFreezes)?;
|
||||
}
|
||||
Self::update_freezes(who, locks.as_bounded_slice())
|
||||
}
|
||||
|
||||
fn extend_freeze(id: &Self::Id, who: &T::AccountId, amount: Self::Balance) -> DispatchResult {
|
||||
if amount.is_zero() {
|
||||
return Ok(())
|
||||
}
|
||||
let mut locks = Freezes::<T, I>::get(who);
|
||||
if let Some(i) = locks.iter_mut().find(|x| &x.id == id) {
|
||||
i.amount = i.amount.max(amount);
|
||||
} else {
|
||||
locks
|
||||
.try_push(IdAmount { id: *id, amount })
|
||||
.map_err(|_| Error::<T, I>::TooManyFreezes)?;
|
||||
}
|
||||
Self::update_freezes(who, locks.as_bounded_slice())
|
||||
}
|
||||
|
||||
fn thaw(id: &Self::Id, who: &T::AccountId) -> DispatchResult {
|
||||
let mut locks = Freezes::<T, I>::get(who);
|
||||
locks.retain(|l| &l.id != id);
|
||||
Self::update_freezes(who, locks.as_bounded_slice())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> fungible::Balanced<T::AccountId> for Pallet<T, I> {
|
||||
type OnDropCredit = fungible::DecreaseIssuance<T::AccountId, Self>;
|
||||
type OnDropDebt = fungible::IncreaseIssuance<T::AccountId, Self>;
|
||||
|
||||
fn done_deposit(who: &T::AccountId, amount: Self::Balance) {
|
||||
Self::deposit_event(Event::<T, I>::Deposit { who: who.clone(), amount });
|
||||
}
|
||||
fn done_withdraw(who: &T::AccountId, amount: Self::Balance) {
|
||||
Self::deposit_event(Event::<T, I>::Withdraw { who: who.clone(), amount });
|
||||
}
|
||||
fn done_issue(amount: Self::Balance) {
|
||||
Self::deposit_event(Event::<T, I>::Issued { amount });
|
||||
}
|
||||
fn done_rescind(amount: Self::Balance) {
|
||||
Self::deposit_event(Event::<T, I>::Rescinded { amount });
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> fungible::BalancedHold<T::AccountId> for Pallet<T, I> {}
|
||||
+692
-1781
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,224 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Tests regarding the functionality of the dispatchables/extrinsics.
|
||||
|
||||
use super::*;
|
||||
use frame_support::traits::tokens::Preservation::Expendable;
|
||||
use fungible::{hold::Mutate as HoldMutate, Inspect, Mutate};
|
||||
|
||||
#[test]
|
||||
fn default_indexing_on_new_accounts_should_not_work2() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(10)
|
||||
.monied(true)
|
||||
.build_and_execute_with(|| {
|
||||
// account 5 should not exist
|
||||
// ext_deposit is 10, value is 9, not satisfies for ext_deposit
|
||||
assert_noop!(
|
||||
Balances::transfer_allow_death(Some(1).into(), 5, 9),
|
||||
TokenError::BelowMinimum,
|
||||
);
|
||||
assert_eq!(Balances::free_balance(1), 100);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dust_account_removal_should_work() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(100)
|
||||
.monied(true)
|
||||
.build_and_execute_with(|| {
|
||||
System::inc_account_nonce(&2);
|
||||
assert_eq!(System::account_nonce(&2), 1);
|
||||
assert_eq!(Balances::total_balance(&2), 2000);
|
||||
// index 1 (account 2) becomes zombie
|
||||
assert_ok!(Balances::transfer_allow_death(Some(2).into(), 5, 1901));
|
||||
assert_eq!(Balances::total_balance(&2), 0);
|
||||
assert_eq!(Balances::total_balance(&5), 1901);
|
||||
assert_eq!(System::account_nonce(&2), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn balance_transfer_works() {
|
||||
ExtBuilder::default().build_and_execute_with(|| {
|
||||
let _ = Balances::mint_into(&1, 111);
|
||||
assert_ok!(Balances::transfer_allow_death(Some(1).into(), 2, 69));
|
||||
assert_eq!(Balances::total_balance(&1), 42);
|
||||
assert_eq!(Balances::total_balance(&2), 69);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_transfer_works() {
|
||||
ExtBuilder::default().build_and_execute_with(|| {
|
||||
let _ = Balances::mint_into(&1, 111);
|
||||
assert_noop!(Balances::force_transfer(Some(2).into(), 1, 2, 69), BadOrigin,);
|
||||
assert_ok!(Balances::force_transfer(RawOrigin::Root.into(), 1, 2, 69));
|
||||
assert_eq!(Balances::total_balance(&1), 42);
|
||||
assert_eq!(Balances::total_balance(&2), 69);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn balance_transfer_when_on_hold_should_not_work() {
|
||||
ExtBuilder::default().build_and_execute_with(|| {
|
||||
let _ = Balances::mint_into(&1, 111);
|
||||
assert_ok!(Balances::hold(&TestId::Foo, &1, 69));
|
||||
assert_noop!(
|
||||
Balances::transfer_allow_death(Some(1).into(), 2, 69),
|
||||
TokenError::FundsUnavailable,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_keep_alive_works() {
|
||||
ExtBuilder::default().existential_deposit(1).build_and_execute_with(|| {
|
||||
let _ = Balances::mint_into(&1, 100);
|
||||
assert_noop!(
|
||||
Balances::transfer_keep_alive(Some(1).into(), 2, 100),
|
||||
TokenError::NotExpendable
|
||||
);
|
||||
assert_eq!(Balances::total_balance(&1), 100);
|
||||
assert_eq!(Balances::total_balance(&2), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_keep_alive_all_free_succeed() {
|
||||
ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| {
|
||||
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 1, 300));
|
||||
assert_ok!(Balances::hold(&TestId::Foo, &1, 100));
|
||||
assert_ok!(Balances::transfer_keep_alive(Some(1).into(), 2, 100));
|
||||
assert_eq!(Balances::total_balance(&1), 200);
|
||||
assert_eq!(Balances::total_balance(&2), 100);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_all_works_1() {
|
||||
ExtBuilder::default().existential_deposit(100).build().execute_with(|| {
|
||||
// setup
|
||||
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 1, 200));
|
||||
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 2, 0));
|
||||
// transfer all and allow death
|
||||
assert_ok!(Balances::transfer_all(Some(1).into(), 2, false));
|
||||
assert_eq!(Balances::total_balance(&1), 0);
|
||||
assert_eq!(Balances::total_balance(&2), 200);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_all_works_2() {
|
||||
ExtBuilder::default().existential_deposit(100).build().execute_with(|| {
|
||||
// setup
|
||||
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 1, 200));
|
||||
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 2, 0));
|
||||
// transfer all and keep alive
|
||||
assert_ok!(Balances::transfer_all(Some(1).into(), 2, true));
|
||||
assert_eq!(Balances::total_balance(&1), 100);
|
||||
assert_eq!(Balances::total_balance(&2), 100);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_all_works_3() {
|
||||
ExtBuilder::default().existential_deposit(100).build().execute_with(|| {
|
||||
// setup
|
||||
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 1, 210));
|
||||
assert_ok!(Balances::hold(&TestId::Foo, &1, 10));
|
||||
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 2, 0));
|
||||
// transfer all and allow death w/ reserved
|
||||
assert_ok!(Balances::transfer_all(Some(1).into(), 2, false));
|
||||
assert_eq!(Balances::total_balance(&1), 110);
|
||||
assert_eq!(Balances::total_balance(&2), 100);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_all_works_4() {
|
||||
ExtBuilder::default().existential_deposit(100).build().execute_with(|| {
|
||||
// setup
|
||||
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 1, 210));
|
||||
assert_ok!(Balances::hold(&TestId::Foo, &1, 10));
|
||||
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 2, 0));
|
||||
// transfer all and keep alive w/ reserved
|
||||
assert_ok!(Balances::transfer_all(Some(1).into(), 2, true));
|
||||
assert_eq!(Balances::total_balance(&1), 110);
|
||||
assert_eq!(Balances::total_balance(&2), 100);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_balance_handles_killing_account() {
|
||||
ExtBuilder::default().build_and_execute_with(|| {
|
||||
let _ = Balances::mint_into(&1, 111);
|
||||
assert_ok!(frame_system::Pallet::<Test>::inc_consumers(&1));
|
||||
assert_noop!(
|
||||
Balances::force_set_balance(RuntimeOrigin::root(), 1, 0),
|
||||
DispatchError::ConsumerRemaining,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_balance_handles_total_issuance() {
|
||||
ExtBuilder::default().build_and_execute_with(|| {
|
||||
let old_total_issuance = Balances::total_issuance();
|
||||
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 1337, 69));
|
||||
assert_eq!(Balances::total_issuance(), old_total_issuance + 69);
|
||||
assert_eq!(Balances::total_balance(&1337), 69);
|
||||
assert_eq!(Balances::free_balance(&1337), 69);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upgrade_accounts_should_work() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(1)
|
||||
.monied(true)
|
||||
.build_and_execute_with(|| {
|
||||
System::inc_providers(&7);
|
||||
assert_ok!(<Test as Config>::AccountStore::try_mutate_exists(
|
||||
&7,
|
||||
|a| -> DispatchResult {
|
||||
*a = Some(AccountData {
|
||||
free: 5,
|
||||
reserved: 5,
|
||||
frozen: Zero::zero(),
|
||||
flags: crate::types::ExtraFlags::old_logic(),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
));
|
||||
assert!(!Balances::account(&7).flags.is_new_logic());
|
||||
assert_eq!(System::providers(&7), 1);
|
||||
assert_eq!(System::consumers(&7), 0);
|
||||
assert_ok!(Balances::upgrade_accounts(Some(1).into(), vec![7]));
|
||||
assert!(Balances::account(&7).flags.is_new_logic());
|
||||
assert_eq!(System::providers(&7), 1);
|
||||
assert_eq!(System::consumers(&7), 1);
|
||||
|
||||
<Balances as frame_support::traits::ReservableCurrency<_>>::unreserve(&7, 5);
|
||||
assert_ok!(<Balances as fungible::Mutate<_>>::transfer(&7, &1, 10, Expendable));
|
||||
assert_eq!(Balances::total_balance(&7), 0);
|
||||
assert_eq!(System::providers(&7), 0);
|
||||
assert_eq!(System::consumers(&7), 0);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Tests regarding the functionality of the `fungible` trait set implementations.
|
||||
|
||||
use super::*;
|
||||
use frame_support::traits::tokens::{
|
||||
Fortitude::{Force, Polite},
|
||||
Precision::{BestEffort, Exact},
|
||||
Preservation::{Expendable, Preserve, Protect},
|
||||
Restriction::Free,
|
||||
};
|
||||
use fungible::{Inspect, InspectFreeze, InspectHold, Mutate, MutateFreeze, MutateHold, Unbalanced};
|
||||
|
||||
#[test]
|
||||
fn inspect_trait_reducible_balance_basic_works() {
|
||||
ExtBuilder::default().existential_deposit(10).build_and_execute_with(|| {
|
||||
Balances::set_balance(&1, 100);
|
||||
assert_eq!(Balances::reducible_balance(&1, Expendable, Polite), 100);
|
||||
assert_eq!(Balances::reducible_balance(&1, Protect, Polite), 90);
|
||||
assert_eq!(Balances::reducible_balance(&1, Preserve, Polite), 90);
|
||||
assert_eq!(Balances::reducible_balance(&1, Expendable, Force), 100);
|
||||
assert_eq!(Balances::reducible_balance(&1, Protect, Force), 90);
|
||||
assert_eq!(Balances::reducible_balance(&1, Preserve, Force), 90);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inspect_trait_reducible_balance_other_provide_works() {
|
||||
ExtBuilder::default().existential_deposit(10).build_and_execute_with(|| {
|
||||
Balances::set_balance(&1, 100);
|
||||
System::inc_providers(&1);
|
||||
assert_eq!(Balances::reducible_balance(&1, Expendable, Polite), 100);
|
||||
assert_eq!(Balances::reducible_balance(&1, Protect, Polite), 100);
|
||||
assert_eq!(Balances::reducible_balance(&1, Preserve, Polite), 90);
|
||||
assert_eq!(Balances::reducible_balance(&1, Expendable, Force), 100);
|
||||
assert_eq!(Balances::reducible_balance(&1, Protect, Force), 100);
|
||||
assert_eq!(Balances::reducible_balance(&1, Preserve, Force), 90);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inspect_trait_reducible_balance_frozen_works() {
|
||||
ExtBuilder::default().existential_deposit(10).build_and_execute_with(|| {
|
||||
Balances::set_balance(&1, 100);
|
||||
assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 50));
|
||||
assert_eq!(Balances::reducible_balance(&1, Expendable, Polite), 50);
|
||||
assert_eq!(Balances::reducible_balance(&1, Protect, Polite), 50);
|
||||
assert_eq!(Balances::reducible_balance(&1, Preserve, Polite), 50);
|
||||
assert_eq!(Balances::reducible_balance(&1, Expendable, Force), 90);
|
||||
assert_eq!(Balances::reducible_balance(&1, Protect, Force), 90);
|
||||
assert_eq!(Balances::reducible_balance(&1, Preserve, Force), 90);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbalanced_trait_set_balance_works() {
|
||||
ExtBuilder::default().build_and_execute_with(|| {
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::balance(&1337), 0);
|
||||
assert_ok!(Balances::write_balance(&1337, 100));
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::balance(&1337), 100);
|
||||
|
||||
assert_ok!(<Balances as fungible::MutateHold<_>>::hold(&TestId::Foo, &1337, 60));
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::balance(&1337), 40);
|
||||
assert_eq!(<Balances as fungible::InspectHold<_>>::total_balance_on_hold(&1337), 60);
|
||||
assert_eq!(
|
||||
<Balances as fungible::InspectHold<_>>::balance_on_hold(&TestId::Foo, &1337),
|
||||
60
|
||||
);
|
||||
|
||||
assert_noop!(Balances::write_balance(&1337, 0), Error::<Test>::InsufficientBalance);
|
||||
|
||||
assert_ok!(Balances::write_balance(&1337, 1));
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::balance(&1337), 1);
|
||||
assert_eq!(
|
||||
<Balances as fungible::InspectHold<_>>::balance_on_hold(&TestId::Foo, &1337),
|
||||
60
|
||||
);
|
||||
|
||||
assert_ok!(<Balances as fungible::MutateHold<_>>::release(&TestId::Foo, &1337, 60, Exact));
|
||||
assert_eq!(<Balances as fungible::InspectHold<_>>::balance_on_hold(&TestId::Foo, &1337), 0);
|
||||
assert_eq!(<Balances as fungible::InspectHold<_>>::total_balance_on_hold(&1337), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbalanced_trait_set_total_issuance_works() {
|
||||
ExtBuilder::default().build_and_execute_with(|| {
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::total_issuance(), 0);
|
||||
Balances::set_total_issuance(100);
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::total_issuance(), 100);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbalanced_trait_decrease_balance_simple_works() {
|
||||
ExtBuilder::default().build_and_execute_with(|| {
|
||||
// An Account that starts at 100
|
||||
assert_ok!(Balances::write_balance(&1337, 100));
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::balance(&1337), 100);
|
||||
// and reserves 50
|
||||
assert_ok!(<Balances as fungible::MutateHold<_>>::hold(&TestId::Foo, &1337, 50));
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::balance(&1337), 50);
|
||||
// and is decreased by 20
|
||||
assert_ok!(Balances::decrease_balance(&1337, 20, Exact, Expendable, Polite));
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::balance(&1337), 30);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbalanced_trait_decrease_balance_works_1() {
|
||||
ExtBuilder::default().build_and_execute_with(|| {
|
||||
assert_ok!(Balances::write_balance(&1337, 100));
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::balance(&1337), 100);
|
||||
|
||||
assert_noop!(
|
||||
Balances::decrease_balance(&1337, 101, Exact, Expendable, Polite),
|
||||
TokenError::FundsUnavailable
|
||||
);
|
||||
assert_eq!(Balances::decrease_balance(&1337, 100, Exact, Expendable, Polite), Ok(100));
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::balance(&1337), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbalanced_trait_decrease_balance_works_2() {
|
||||
ExtBuilder::default().build_and_execute_with(|| {
|
||||
// free: 40, reserved: 60
|
||||
assert_ok!(Balances::write_balance(&1337, 100));
|
||||
assert_ok!(Balances::hold(&TestId::Foo, &1337, 60));
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::balance(&1337), 40);
|
||||
assert_eq!(Balances::total_balance_on_hold(&1337), 60);
|
||||
assert_noop!(
|
||||
Balances::decrease_balance(&1337, 40, Exact, Expendable, Polite),
|
||||
Error::<Test>::InsufficientBalance
|
||||
);
|
||||
assert_eq!(Balances::decrease_balance(&1337, 39, Exact, Expendable, Polite), Ok(39));
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::balance(&1337), 1);
|
||||
assert_eq!(Balances::total_balance_on_hold(&1337), 60);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbalanced_trait_decrease_balance_at_most_works_1() {
|
||||
ExtBuilder::default().build_and_execute_with(|| {
|
||||
assert_ok!(Balances::write_balance(&1337, 100));
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::balance(&1337), 100);
|
||||
|
||||
assert_eq!(Balances::decrease_balance(&1337, 101, BestEffort, Expendable, Polite), Ok(100));
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::balance(&1337), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbalanced_trait_decrease_balance_at_most_works_2() {
|
||||
ExtBuilder::default().build_and_execute_with(|| {
|
||||
assert_ok!(Balances::write_balance(&1337, 99));
|
||||
assert_eq!(Balances::decrease_balance(&1337, 99, BestEffort, Expendable, Polite), Ok(99));
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::balance(&1337), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbalanced_trait_decrease_balance_at_most_works_3() {
|
||||
ExtBuilder::default().build_and_execute_with(|| {
|
||||
// free: 40, reserved: 60
|
||||
assert_ok!(Balances::write_balance(&1337, 100));
|
||||
assert_ok!(Balances::hold(&TestId::Foo, &1337, 60));
|
||||
assert_eq!(Balances::free_balance(1337), 40);
|
||||
assert_eq!(Balances::total_balance_on_hold(&1337), 60);
|
||||
assert_eq!(Balances::decrease_balance(&1337, 0, BestEffort, Expendable, Polite), Ok(0));
|
||||
assert_eq!(Balances::free_balance(1337), 40);
|
||||
assert_eq!(Balances::total_balance_on_hold(&1337), 60);
|
||||
assert_eq!(Balances::decrease_balance(&1337, 10, BestEffort, Expendable, Polite), Ok(10));
|
||||
assert_eq!(Balances::free_balance(1337), 30);
|
||||
assert_eq!(Balances::decrease_balance(&1337, 200, BestEffort, Expendable, Polite), Ok(29));
|
||||
assert_eq!(<Balances as fungible::Inspect<_>>::balance(&1337), 1);
|
||||
assert_eq!(Balances::free_balance(1337), 1);
|
||||
assert_eq!(Balances::total_balance_on_hold(&1337), 60);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbalanced_trait_increase_balance_works() {
|
||||
ExtBuilder::default().build_and_execute_with(|| {
|
||||
assert_noop!(Balances::increase_balance(&1337, 0, Exact), TokenError::BelowMinimum);
|
||||
assert_eq!(Balances::increase_balance(&1337, 1, Exact), Ok(1));
|
||||
assert_noop!(Balances::increase_balance(&1337, u64::MAX, Exact), ArithmeticError::Overflow);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbalanced_trait_increase_balance_at_most_works() {
|
||||
ExtBuilder::default().build_and_execute_with(|| {
|
||||
assert_eq!(Balances::increase_balance(&1337, 0, BestEffort), Ok(0));
|
||||
assert_eq!(Balances::increase_balance(&1337, 1, BestEffort), Ok(1));
|
||||
assert_eq!(Balances::increase_balance(&1337, u64::MAX, BestEffort), Ok(u64::MAX - 1));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freezing_and_holds_should_overlap() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(1)
|
||||
.monied(true)
|
||||
.build_and_execute_with(|| {
|
||||
assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 10));
|
||||
assert_ok!(Balances::hold(&TestId::Foo, &1, 9));
|
||||
assert_eq!(Balances::account(&1).free, 1);
|
||||
assert_eq!(System::consumers(&1), 1);
|
||||
assert_eq!(Balances::account(&1).free, 1);
|
||||
assert_eq!(Balances::account(&1).frozen, 10);
|
||||
assert_eq!(Balances::account(&1).reserved, 9);
|
||||
assert_eq!(Balances::total_balance_on_hold(&1), 9);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_hold_balance_cannot_be_moved_without_force() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(1)
|
||||
.monied(true)
|
||||
.build_and_execute_with(|| {
|
||||
assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 10));
|
||||
assert_ok!(Balances::hold(&TestId::Foo, &1, 9));
|
||||
assert_eq!(Balances::reducible_total_balance_on_hold(&1, Force), 9);
|
||||
assert_eq!(Balances::reducible_total_balance_on_hold(&1, Polite), 0);
|
||||
let e = TokenError::Frozen;
|
||||
assert_noop!(
|
||||
Balances::transfer_on_hold(&TestId::Foo, &1, &2, 1, Exact, Free, Polite),
|
||||
e
|
||||
);
|
||||
assert_ok!(Balances::transfer_on_hold(&TestId::Foo, &1, &2, 1, Exact, Free, Force));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_hold_balance_best_effort_transfer_works() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(1)
|
||||
.monied(true)
|
||||
.build_and_execute_with(|| {
|
||||
assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 5));
|
||||
assert_ok!(Balances::hold(&TestId::Foo, &1, 9));
|
||||
assert_eq!(Balances::reducible_total_balance_on_hold(&1, Force), 9);
|
||||
assert_eq!(Balances::reducible_total_balance_on_hold(&1, Polite), 5);
|
||||
assert_ok!(Balances::transfer_on_hold(
|
||||
&TestId::Foo,
|
||||
&1,
|
||||
&2,
|
||||
10,
|
||||
BestEffort,
|
||||
Free,
|
||||
Polite
|
||||
));
|
||||
assert_eq!(Balances::total_balance(&1), 5);
|
||||
assert_eq!(Balances::total_balance(&2), 25);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_freezing_should_work() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(1)
|
||||
.monied(true)
|
||||
.build_and_execute_with(|| {
|
||||
assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 5));
|
||||
assert_eq!(System::consumers(&1), 1);
|
||||
assert_ok!(<Balances as fungible::Mutate<_>>::transfer(&1, &2, 5, Expendable));
|
||||
assert_noop!(
|
||||
<Balances as fungible::Mutate<_>>::transfer(&1, &2, 1, Expendable),
|
||||
TokenError::Frozen
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thaw_should_work() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(1)
|
||||
.monied(true)
|
||||
.build_and_execute_with(|| {
|
||||
assert_ok!(Balances::set_freeze(&TestId::Foo, &1, u64::MAX));
|
||||
assert_ok!(Balances::thaw(&TestId::Foo, &1));
|
||||
assert_eq!(System::consumers(&1), 0);
|
||||
assert_eq!(Balances::balance_frozen(&TestId::Foo, &1), 0);
|
||||
assert_eq!(Balances::account(&1).frozen, 0);
|
||||
assert_ok!(<Balances as fungible::Mutate<_>>::transfer(&1, &2, 10, Expendable));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_freeze_zero_should_work() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(1)
|
||||
.monied(true)
|
||||
.build_and_execute_with(|| {
|
||||
assert_ok!(Balances::set_freeze(&TestId::Foo, &1, u64::MAX));
|
||||
assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 0));
|
||||
assert_eq!(System::consumers(&1), 0);
|
||||
assert_eq!(Balances::balance_frozen(&TestId::Foo, &1), 0);
|
||||
assert_eq!(Balances::account(&1).frozen, 0);
|
||||
assert_ok!(<Balances as fungible::Mutate<_>>::transfer(&1, &2, 10, Expendable));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_freeze_should_work() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(1)
|
||||
.monied(true)
|
||||
.build_and_execute_with(|| {
|
||||
assert_ok!(Balances::set_freeze(&TestId::Foo, &1, u64::MAX));
|
||||
assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 5));
|
||||
assert_ok!(<Balances as fungible::Mutate<_>>::transfer(&1, &2, 5, Expendable));
|
||||
assert_noop!(
|
||||
<Balances as fungible::Mutate<_>>::transfer(&1, &2, 1, Expendable),
|
||||
TokenError::Frozen
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_freeze_should_work() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(1)
|
||||
.monied(true)
|
||||
.build_and_execute_with(|| {
|
||||
assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 5));
|
||||
assert_ok!(Balances::extend_freeze(&TestId::Foo, &1, 10));
|
||||
assert_eq!(Balances::account(&1).frozen, 10);
|
||||
assert_eq!(Balances::balance_frozen(&TestId::Foo, &1), 10);
|
||||
assert_noop!(
|
||||
<Balances as fungible::Mutate<_>>::transfer(&1, &2, 1, Expendable),
|
||||
TokenError::Frozen
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_freezing_should_work() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(1)
|
||||
.monied(true)
|
||||
.build_and_execute_with(|| {
|
||||
assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 5));
|
||||
assert_ok!(Balances::set_freeze(&TestId::Bar, &1, 5));
|
||||
assert_eq!(System::consumers(&1), 1);
|
||||
assert_ok!(<Balances as fungible::Mutate<_>>::transfer(&1, &2, 5, Expendable));
|
||||
assert_noop!(
|
||||
<Balances as fungible::Mutate<_>>::transfer(&1, &2, 1, Expendable),
|
||||
TokenError::Frozen
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_hold_entire_balance_when_second_provider() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(1)
|
||||
.monied(false)
|
||||
.build_and_execute_with(|| {
|
||||
<Balances as fungible::Mutate<_>>::set_balance(&1, 100);
|
||||
assert_noop!(Balances::hold(&TestId::Foo, &1, 100), TokenError::FundsUnavailable);
|
||||
System::inc_providers(&1);
|
||||
assert_eq!(System::providers(&1), 2);
|
||||
assert_ok!(Balances::hold(&TestId::Foo, &1, 100));
|
||||
assert_eq!(System::providers(&1), 1);
|
||||
assert_noop!(System::dec_providers(&1), DispatchError::ConsumerRemaining);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unholding_frees_hold_slot() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(1)
|
||||
.monied(false)
|
||||
.build_and_execute_with(|| {
|
||||
<Balances as fungible::Mutate<_>>::set_balance(&1, 100);
|
||||
assert_ok!(Balances::hold(&TestId::Foo, &1, 10));
|
||||
assert_ok!(Balances::hold(&TestId::Bar, &1, 10));
|
||||
assert_ok!(Balances::release(&TestId::Foo, &1, 10, Exact));
|
||||
assert_ok!(Balances::hold(&TestId::Baz, &1, 10));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2018-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Tests.
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
use crate::{self as pallet_balances, AccountData, Config, CreditOf, Error, Pallet};
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use frame_support::{
|
||||
assert_err, assert_noop, assert_ok, assert_storage_noop,
|
||||
dispatch::DispatchInfo,
|
||||
parameter_types,
|
||||
traits::{
|
||||
tokens::fungible, ConstU32, ConstU64, ConstU8, Imbalance as ImbalanceT, OnUnbalanced,
|
||||
StorageMapShim, StoredMap,
|
||||
},
|
||||
weights::{IdentityFee, Weight},
|
||||
RuntimeDebug,
|
||||
};
|
||||
use frame_system::{self as system, RawOrigin};
|
||||
use pallet_transaction_payment::{ChargeTransactionPayment, CurrencyAdapter, Multiplier};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::H256;
|
||||
use sp_io;
|
||||
use sp_runtime::{
|
||||
testing::Header,
|
||||
traits::{BadOrigin, IdentityLookup, SignedExtension, Zero},
|
||||
ArithmeticError, DispatchError, DispatchResult, FixedPointNumber, TokenError,
|
||||
};
|
||||
|
||||
mod currency_tests;
|
||||
mod dispatchable_tests;
|
||||
mod fungible_tests;
|
||||
mod reentrancy_tests;
|
||||
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
|
||||
type Block = frame_system::mocking::MockBlock<Test>;
|
||||
|
||||
#[derive(
|
||||
Encode,
|
||||
Decode,
|
||||
Copy,
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
MaxEncodedLen,
|
||||
TypeInfo,
|
||||
RuntimeDebug,
|
||||
)]
|
||||
pub enum TestId {
|
||||
Foo,
|
||||
Bar,
|
||||
Baz,
|
||||
}
|
||||
|
||||
frame_support::construct_runtime!(
|
||||
pub struct Test where
|
||||
Block = Block,
|
||||
NodeBlock = Block,
|
||||
UncheckedExtrinsic = UncheckedExtrinsic,
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
|
||||
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
|
||||
TransactionPayment: pallet_transaction_payment::{Pallet, Storage, Event<T>},
|
||||
}
|
||||
);
|
||||
|
||||
parameter_types! {
|
||||
pub BlockWeights: frame_system::limits::BlockWeights =
|
||||
frame_system::limits::BlockWeights::simple_max(
|
||||
frame_support::weights::Weight::from_parts(1024, u64::MAX),
|
||||
);
|
||||
pub static ExistentialDeposit: u64 = 0;
|
||||
}
|
||||
impl frame_system::Config for Test {
|
||||
type BaseCallFilter = frame_support::traits::Everything;
|
||||
type BlockWeights = BlockWeights;
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type Index = u64;
|
||||
type BlockNumber = u64;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Hash = H256;
|
||||
type Hashing = ::sp_runtime::traits::BlakeTwo256;
|
||||
type AccountId = u64;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = Header;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = ConstU64<250>;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = super::AccountData<u64>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = frame_support::traits::ConstU32<16>;
|
||||
}
|
||||
|
||||
impl pallet_transaction_payment::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type OnChargeTransaction = CurrencyAdapter<Pallet<Test>, ()>;
|
||||
type OperationalFeeMultiplier = ConstU8<5>;
|
||||
type WeightToFee = IdentityFee<u64>;
|
||||
type LengthToFee = IdentityFee<u64>;
|
||||
type FeeMultiplierUpdate = ();
|
||||
}
|
||||
|
||||
impl Config for Test {
|
||||
type Balance = u64;
|
||||
type DustRemoval = DustTrap;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = TestAccountStore;
|
||||
type MaxLocks = ConstU32<50>;
|
||||
type MaxReserves = ConstU32<2>;
|
||||
type ReserveIdentifier = TestId;
|
||||
type WeightInfo = ();
|
||||
type HoldIdentifier = TestId;
|
||||
type FreezeIdentifier = TestId;
|
||||
type MaxFreezes = ConstU32<2>;
|
||||
type MaxHolds = ConstU32<2>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ExtBuilder {
|
||||
existential_deposit: u64,
|
||||
monied: bool,
|
||||
dust_trap: Option<u64>,
|
||||
}
|
||||
impl Default for ExtBuilder {
|
||||
fn default() -> Self {
|
||||
Self { existential_deposit: 1, monied: false, dust_trap: None }
|
||||
}
|
||||
}
|
||||
impl ExtBuilder {
|
||||
pub fn existential_deposit(mut self, existential_deposit: u64) -> Self {
|
||||
self.existential_deposit = existential_deposit;
|
||||
self
|
||||
}
|
||||
pub fn monied(mut self, monied: bool) -> Self {
|
||||
self.monied = monied;
|
||||
if self.existential_deposit == 0 {
|
||||
self.existential_deposit = 1;
|
||||
}
|
||||
self
|
||||
}
|
||||
pub fn dust_trap(mut self, account: u64) -> Self {
|
||||
self.dust_trap = Some(account);
|
||||
self
|
||||
}
|
||||
pub fn set_associated_consts(&self) {
|
||||
DUST_TRAP_TARGET.with(|v| v.replace(self.dust_trap));
|
||||
EXISTENTIAL_DEPOSIT.with(|v| v.replace(self.existential_deposit));
|
||||
}
|
||||
pub fn build(self) -> sp_io::TestExternalities {
|
||||
self.set_associated_consts();
|
||||
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
|
||||
pallet_balances::GenesisConfig::<Test> {
|
||||
balances: if self.monied {
|
||||
vec![
|
||||
(1, 10 * self.existential_deposit),
|
||||
(2, 20 * self.existential_deposit),
|
||||
(3, 30 * self.existential_deposit),
|
||||
(4, 40 * self.existential_deposit),
|
||||
(12, 10 * self.existential_deposit),
|
||||
]
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
let mut ext = sp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| System::set_block_number(1));
|
||||
ext
|
||||
}
|
||||
pub fn build_and_execute_with(self, f: impl Fn()) {
|
||||
let other = self.clone();
|
||||
UseSystem::set(false);
|
||||
other.build().execute_with(|| f());
|
||||
UseSystem::set(true);
|
||||
self.build().execute_with(|| f());
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
static DustTrapTarget: Option<u64> = None;
|
||||
}
|
||||
|
||||
pub struct DustTrap;
|
||||
|
||||
impl OnUnbalanced<CreditOf<Test, ()>> for DustTrap {
|
||||
fn on_nonzero_unbalanced(amount: CreditOf<Test, ()>) {
|
||||
match DustTrapTarget::get() {
|
||||
None => drop(amount),
|
||||
Some(a) => {
|
||||
let result = <Balances as fungible::Balanced<_>>::resolve(&a, amount);
|
||||
debug_assert!(result.is_ok());
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub static UseSystem: bool = false;
|
||||
}
|
||||
|
||||
type BalancesAccountStore = StorageMapShim<super::Account<Test>, u64, super::AccountData<u64>>;
|
||||
type SystemAccountStore = frame_system::Pallet<Test>;
|
||||
|
||||
pub struct TestAccountStore;
|
||||
impl StoredMap<u64, super::AccountData<u64>> for TestAccountStore {
|
||||
fn get(k: &u64) -> super::AccountData<u64> {
|
||||
if UseSystem::get() {
|
||||
<SystemAccountStore as StoredMap<_, _>>::get(k)
|
||||
} else {
|
||||
<BalancesAccountStore as StoredMap<_, _>>::get(k)
|
||||
}
|
||||
}
|
||||
fn try_mutate_exists<R, E: From<DispatchError>>(
|
||||
k: &u64,
|
||||
f: impl FnOnce(&mut Option<super::AccountData<u64>>) -> Result<R, E>,
|
||||
) -> Result<R, E> {
|
||||
if UseSystem::get() {
|
||||
<SystemAccountStore as StoredMap<_, _>>::try_mutate_exists(k, f)
|
||||
} else {
|
||||
<BalancesAccountStore as StoredMap<_, _>>::try_mutate_exists(k, f)
|
||||
}
|
||||
}
|
||||
fn mutate<R>(
|
||||
k: &u64,
|
||||
f: impl FnOnce(&mut super::AccountData<u64>) -> R,
|
||||
) -> Result<R, DispatchError> {
|
||||
if UseSystem::get() {
|
||||
<SystemAccountStore as StoredMap<_, _>>::mutate(k, f)
|
||||
} else {
|
||||
<BalancesAccountStore as StoredMap<_, _>>::mutate(k, f)
|
||||
}
|
||||
}
|
||||
fn mutate_exists<R>(
|
||||
k: &u64,
|
||||
f: impl FnOnce(&mut Option<super::AccountData<u64>>) -> R,
|
||||
) -> Result<R, DispatchError> {
|
||||
if UseSystem::get() {
|
||||
<SystemAccountStore as StoredMap<_, _>>::mutate_exists(k, f)
|
||||
} else {
|
||||
<BalancesAccountStore as StoredMap<_, _>>::mutate_exists(k, f)
|
||||
}
|
||||
}
|
||||
fn insert(k: &u64, t: super::AccountData<u64>) -> Result<(), DispatchError> {
|
||||
if UseSystem::get() {
|
||||
<SystemAccountStore as StoredMap<_, _>>::insert(k, t)
|
||||
} else {
|
||||
<BalancesAccountStore as StoredMap<_, _>>::insert(k, t)
|
||||
}
|
||||
}
|
||||
fn remove(k: &u64) -> Result<(), DispatchError> {
|
||||
if UseSystem::get() {
|
||||
<SystemAccountStore as StoredMap<_, _>>::remove(k)
|
||||
} else {
|
||||
<BalancesAccountStore as StoredMap<_, _>>::remove(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn events() -> Vec<RuntimeEvent> {
|
||||
let evt = System::events().into_iter().map(|evt| evt.event).collect::<Vec<_>>();
|
||||
System::reset_events();
|
||||
evt
|
||||
}
|
||||
|
||||
/// create a transaction info struct from weight. Handy to avoid building the whole struct.
|
||||
pub fn info_from_weight(w: Weight) -> DispatchInfo {
|
||||
DispatchInfo { weight: w, ..Default::default() }
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Tests regarding the reentrancy functionality.
|
||||
|
||||
use super::*;
|
||||
use frame_support::traits::tokens::{
|
||||
Fortitude::Force,
|
||||
Precision::BestEffort,
|
||||
Preservation::{Expendable, Protect},
|
||||
};
|
||||
use fungible::Balanced;
|
||||
|
||||
#[test]
|
||||
fn transfer_dust_removal_tst1_should_work() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(100)
|
||||
.dust_trap(1)
|
||||
.build_and_execute_with(|| {
|
||||
// Verification of reentrancy in dust removal
|
||||
assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 1, 1000));
|
||||
assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 2, 500));
|
||||
|
||||
// In this transaction, account 2 free balance
|
||||
// drops below existential balance
|
||||
// and dust balance is removed from account 2
|
||||
assert_ok!(Balances::transfer_allow_death(RawOrigin::Signed(2).into(), 3, 450));
|
||||
|
||||
// As expected dust balance is removed.
|
||||
assert_eq!(Balances::free_balance(&2), 0);
|
||||
|
||||
// As expected beneficiary account 3
|
||||
// received the transfered fund.
|
||||
assert_eq!(Balances::free_balance(&3), 450);
|
||||
|
||||
// Dust balance is deposited to account 1
|
||||
// during the process of dust removal.
|
||||
assert_eq!(Balances::free_balance(&1), 1050);
|
||||
|
||||
// Verify the events
|
||||
assert_eq!(System::events().len(), 12);
|
||||
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer {
|
||||
from: 2,
|
||||
to: 3,
|
||||
amount: 450,
|
||||
}));
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost {
|
||||
account: 2,
|
||||
amount: 50,
|
||||
}));
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit {
|
||||
who: 1,
|
||||
amount: 50,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_dust_removal_tst2_should_work() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(100)
|
||||
.dust_trap(1)
|
||||
.build_and_execute_with(|| {
|
||||
// Verification of reentrancy in dust removal
|
||||
assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 1, 1000));
|
||||
assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 2, 500));
|
||||
|
||||
// In this transaction, account 2 free balance
|
||||
// drops below existential balance
|
||||
// and dust balance is removed from account 2
|
||||
assert_ok!(Balances::transfer_allow_death(RawOrigin::Signed(2).into(), 1, 450));
|
||||
|
||||
// As expected dust balance is removed.
|
||||
assert_eq!(Balances::free_balance(&2), 0);
|
||||
|
||||
// Dust balance is deposited to account 1
|
||||
// during the process of dust removal.
|
||||
assert_eq!(Balances::free_balance(&1), 1500);
|
||||
|
||||
// Verify the events
|
||||
assert_eq!(System::events().len(), 10);
|
||||
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer {
|
||||
from: 2,
|
||||
to: 1,
|
||||
amount: 450,
|
||||
}));
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost {
|
||||
account: 2,
|
||||
amount: 50,
|
||||
}));
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit {
|
||||
who: 1,
|
||||
amount: 50,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repatriating_reserved_balance_dust_removal_should_work() {
|
||||
ExtBuilder::default()
|
||||
.existential_deposit(100)
|
||||
.dust_trap(1)
|
||||
.build_and_execute_with(|| {
|
||||
// Verification of reentrancy in dust removal
|
||||
assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 1, 1000));
|
||||
assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 2, 500));
|
||||
|
||||
// Reserve a value on account 2,
|
||||
// Such that free balance is lower than
|
||||
// Exestintial deposit.
|
||||
assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(2), 1, 450));
|
||||
|
||||
// Since free balance of account 2 is lower than
|
||||
// existential deposit, dust amount is
|
||||
// removed from the account 2
|
||||
assert_eq!(Balances::reserved_balance(2), 0);
|
||||
assert_eq!(Balances::free_balance(2), 0);
|
||||
|
||||
// account 1 is credited with reserved amount
|
||||
// together with dust balance during dust
|
||||
// removal.
|
||||
assert_eq!(Balances::reserved_balance(1), 0);
|
||||
assert_eq!(Balances::free_balance(1), 1500);
|
||||
|
||||
// Verify the events
|
||||
assert_eq!(System::events().len(), 10);
|
||||
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer {
|
||||
from: 2,
|
||||
to: 1,
|
||||
amount: 450,
|
||||
}));
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost {
|
||||
account: 2,
|
||||
amount: 50,
|
||||
}));
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit {
|
||||
who: 1,
|
||||
amount: 50,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_events_with_no_existential_deposit_suicide_with_dust() {
|
||||
ExtBuilder::default().existential_deposit(2).build_and_execute_with(|| {
|
||||
assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 1, 100));
|
||||
|
||||
assert_eq!(
|
||||
events(),
|
||||
[
|
||||
RuntimeEvent::System(system::Event::NewAccount { account: 1 }),
|
||||
RuntimeEvent::Balances(crate::Event::Endowed { account: 1, free_balance: 100 }),
|
||||
RuntimeEvent::Balances(crate::Event::BalanceSet { who: 1, free: 100 }),
|
||||
]
|
||||
);
|
||||
|
||||
let res = Balances::withdraw(&1, 98, BestEffort, Protect, Force);
|
||||
assert_eq!(res.unwrap().peek(), 98);
|
||||
|
||||
// no events
|
||||
assert_eq!(
|
||||
events(),
|
||||
[RuntimeEvent::Balances(crate::Event::Withdraw { who: 1, amount: 98 })]
|
||||
);
|
||||
|
||||
let res = Balances::withdraw(&1, 1, BestEffort, Expendable, Force);
|
||||
assert_eq!(res.unwrap().peek(), 1);
|
||||
|
||||
assert_eq!(
|
||||
events(),
|
||||
[
|
||||
RuntimeEvent::System(system::Event::KilledAccount { account: 1 }),
|
||||
RuntimeEvent::Balances(crate::Event::DustLost { account: 1, amount: 1 }),
|
||||
RuntimeEvent::Balances(crate::Event::Withdraw { who: 1, amount: 1 })
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Test utilities
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
use crate::{self as pallet_balances, decl_tests, Config, Pallet};
|
||||
use frame_support::{
|
||||
dispatch::DispatchInfo,
|
||||
parameter_types,
|
||||
traits::{ConstU32, ConstU64, ConstU8},
|
||||
weights::{IdentityFee, Weight},
|
||||
};
|
||||
use pallet_transaction_payment::CurrencyAdapter;
|
||||
use sp_core::H256;
|
||||
use sp_io;
|
||||
use sp_runtime::{testing::Header, traits::IdentityLookup};
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
|
||||
type Block = frame_system::mocking::MockBlock<Test>;
|
||||
|
||||
frame_support::construct_runtime!(
|
||||
pub enum Test where
|
||||
Block = Block,
|
||||
NodeBlock = Block,
|
||||
UncheckedExtrinsic = UncheckedExtrinsic,
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
|
||||
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
|
||||
TransactionPayment: pallet_transaction_payment::{Pallet, Storage, Event<T>},
|
||||
}
|
||||
);
|
||||
|
||||
parameter_types! {
|
||||
pub BlockWeights: frame_system::limits::BlockWeights =
|
||||
frame_system::limits::BlockWeights::simple_max(
|
||||
frame_support::weights::Weight::from_parts(1024, u64::MAX),
|
||||
);
|
||||
pub static ExistentialDeposit: u64 = 0;
|
||||
}
|
||||
impl frame_system::Config for Test {
|
||||
type BaseCallFilter = frame_support::traits::Everything;
|
||||
type BlockWeights = BlockWeights;
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type Index = u64;
|
||||
type BlockNumber = u64;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Hash = H256;
|
||||
type Hashing = ::sp_runtime::traits::BlakeTwo256;
|
||||
type AccountId = u64;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = Header;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = ConstU64<250>;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = super::AccountData<u64>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = frame_support::traits::ConstU32<16>;
|
||||
}
|
||||
|
||||
impl pallet_transaction_payment::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type OnChargeTransaction = CurrencyAdapter<Pallet<Test>, ()>;
|
||||
type OperationalFeeMultiplier = ConstU8<5>;
|
||||
type WeightToFee = IdentityFee<u64>;
|
||||
type LengthToFee = IdentityFee<u64>;
|
||||
type FeeMultiplierUpdate = ();
|
||||
}
|
||||
|
||||
impl Config for Test {
|
||||
type Balance = u64;
|
||||
type DustRemoval = ();
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = frame_system::Pallet<Test>;
|
||||
type MaxLocks = ();
|
||||
type MaxReserves = ConstU32<2>;
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
pub struct ExtBuilder {
|
||||
existential_deposit: u64,
|
||||
monied: bool,
|
||||
}
|
||||
impl Default for ExtBuilder {
|
||||
fn default() -> Self {
|
||||
Self { existential_deposit: 1, monied: false }
|
||||
}
|
||||
}
|
||||
impl ExtBuilder {
|
||||
pub fn existential_deposit(mut self, existential_deposit: u64) -> Self {
|
||||
self.existential_deposit = existential_deposit;
|
||||
self
|
||||
}
|
||||
pub fn monied(mut self, monied: bool) -> Self {
|
||||
self.monied = monied;
|
||||
self
|
||||
}
|
||||
pub fn set_associated_consts(&self) {
|
||||
EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit);
|
||||
}
|
||||
pub fn build(self) -> sp_io::TestExternalities {
|
||||
self.set_associated_consts();
|
||||
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
|
||||
pallet_balances::GenesisConfig::<Test> {
|
||||
balances: if self.monied {
|
||||
vec![
|
||||
(1, 10 * self.existential_deposit),
|
||||
(2, 20 * self.existential_deposit),
|
||||
(3, 30 * self.existential_deposit),
|
||||
(4, 40 * self.existential_deposit),
|
||||
(12, 10 * self.existential_deposit),
|
||||
]
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
let mut ext = sp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| System::set_block_number(1));
|
||||
ext
|
||||
}
|
||||
}
|
||||
|
||||
decl_tests! { Test, ExtBuilder, EXISTENTIAL_DEPOSIT }
|
||||
@@ -1,191 +0,0 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Test utilities
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
use crate::{self as pallet_balances, decl_tests, Config, Pallet};
|
||||
use frame_support::{
|
||||
dispatch::DispatchInfo,
|
||||
parameter_types,
|
||||
traits::{ConstU32, ConstU64, ConstU8, StorageMapShim},
|
||||
weights::{IdentityFee, Weight},
|
||||
};
|
||||
use pallet_transaction_payment::CurrencyAdapter;
|
||||
use sp_core::H256;
|
||||
use sp_io;
|
||||
use sp_runtime::{testing::Header, traits::IdentityLookup};
|
||||
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
|
||||
type Block = frame_system::mocking::MockBlock<Test>;
|
||||
|
||||
frame_support::construct_runtime!(
|
||||
pub struct Test where
|
||||
Block = Block,
|
||||
NodeBlock = Block,
|
||||
UncheckedExtrinsic = UncheckedExtrinsic,
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
|
||||
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
|
||||
TransactionPayment: pallet_transaction_payment::{Pallet, Storage, Event<T>},
|
||||
}
|
||||
);
|
||||
|
||||
parameter_types! {
|
||||
pub BlockWeights: frame_system::limits::BlockWeights =
|
||||
frame_system::limits::BlockWeights::simple_max(
|
||||
frame_support::weights::Weight::from_parts(1024, u64::MAX),
|
||||
);
|
||||
pub static ExistentialDeposit: u64 = 0;
|
||||
}
|
||||
impl frame_system::Config for Test {
|
||||
type BaseCallFilter = frame_support::traits::Everything;
|
||||
type BlockWeights = BlockWeights;
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type Index = u64;
|
||||
type BlockNumber = u64;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Hash = H256;
|
||||
type Hashing = ::sp_runtime::traits::BlakeTwo256;
|
||||
type AccountId = u64;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = Header;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = ConstU64<250>;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = ();
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = frame_support::traits::ConstU32<16>;
|
||||
}
|
||||
|
||||
impl pallet_transaction_payment::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type OnChargeTransaction = CurrencyAdapter<Pallet<Test>, ()>;
|
||||
type OperationalFeeMultiplier = ConstU8<5>;
|
||||
type WeightToFee = IdentityFee<u64>;
|
||||
type LengthToFee = IdentityFee<u64>;
|
||||
type FeeMultiplierUpdate = ();
|
||||
}
|
||||
|
||||
impl Config for Test {
|
||||
type Balance = u64;
|
||||
type DustRemoval = ();
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore =
|
||||
StorageMapShim<super::Account<Test>, system::Provider<Test>, u64, super::AccountData<u64>>;
|
||||
type MaxLocks = ConstU32<50>;
|
||||
type MaxReserves = ConstU32<2>;
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
pub struct ExtBuilder {
|
||||
existential_deposit: u64,
|
||||
monied: bool,
|
||||
}
|
||||
impl Default for ExtBuilder {
|
||||
fn default() -> Self {
|
||||
Self { existential_deposit: 1, monied: false }
|
||||
}
|
||||
}
|
||||
impl ExtBuilder {
|
||||
pub fn existential_deposit(mut self, existential_deposit: u64) -> Self {
|
||||
self.existential_deposit = existential_deposit;
|
||||
self
|
||||
}
|
||||
pub fn monied(mut self, monied: bool) -> Self {
|
||||
self.monied = monied;
|
||||
if self.existential_deposit == 0 {
|
||||
self.existential_deposit = 1;
|
||||
}
|
||||
self
|
||||
}
|
||||
pub fn set_associated_consts(&self) {
|
||||
EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit);
|
||||
}
|
||||
pub fn build(self) -> sp_io::TestExternalities {
|
||||
self.set_associated_consts();
|
||||
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
|
||||
pallet_balances::GenesisConfig::<Test> {
|
||||
balances: if self.monied {
|
||||
vec![
|
||||
(1, 10 * self.existential_deposit),
|
||||
(2, 20 * self.existential_deposit),
|
||||
(3, 30 * self.existential_deposit),
|
||||
(4, 40 * self.existential_deposit),
|
||||
(12, 10 * self.existential_deposit),
|
||||
]
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
let mut ext = sp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| System::set_block_number(1));
|
||||
ext
|
||||
}
|
||||
}
|
||||
|
||||
decl_tests! { Test, ExtBuilder, EXISTENTIAL_DEPOSIT }
|
||||
|
||||
#[test]
|
||||
fn emit_events_with_no_existential_deposit_suicide_with_dust() {
|
||||
<ExtBuilder>::default().existential_deposit(2).build().execute_with(|| {
|
||||
assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 1, 100, 0));
|
||||
|
||||
assert_eq!(
|
||||
events(),
|
||||
[
|
||||
RuntimeEvent::System(system::Event::NewAccount { account: 1 }),
|
||||
RuntimeEvent::Balances(crate::Event::Endowed { account: 1, free_balance: 100 }),
|
||||
RuntimeEvent::Balances(crate::Event::BalanceSet { who: 1, free: 100, reserved: 0 }),
|
||||
]
|
||||
);
|
||||
|
||||
let res = Balances::slash(&1, 98);
|
||||
assert_eq!(res, (NegativeImbalance::new(98), 0));
|
||||
|
||||
// no events
|
||||
assert_eq!(
|
||||
events(),
|
||||
[RuntimeEvent::Balances(crate::Event::Slashed { who: 1, amount: 98 })]
|
||||
);
|
||||
|
||||
let res = Balances::slash(&1, 1);
|
||||
assert_eq!(res, (NegativeImbalance::new(1), 0));
|
||||
|
||||
assert_eq!(
|
||||
events(),
|
||||
[
|
||||
RuntimeEvent::System(system::Event::KilledAccount { account: 1 }),
|
||||
RuntimeEvent::Balances(crate::Event::DustLost { account: 1, amount: 1 }),
|
||||
RuntimeEvent::Balances(crate::Event::Slashed { who: 1, amount: 1 })
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Test setup for potential reentracy and lost updates of nested mutations.
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
use crate::{self as pallet_balances, Config};
|
||||
use frame_support::{
|
||||
parameter_types,
|
||||
traits::{ConstU32, ConstU64, StorageMapShim},
|
||||
};
|
||||
use sp_core::H256;
|
||||
use sp_io;
|
||||
use sp_runtime::{testing::Header, traits::IdentityLookup};
|
||||
|
||||
use crate::*;
|
||||
use frame_support::{
|
||||
assert_ok,
|
||||
traits::{Currency, ReservableCurrency},
|
||||
};
|
||||
use frame_system::RawOrigin;
|
||||
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
|
||||
type Block = frame_system::mocking::MockBlock<Test>;
|
||||
|
||||
frame_support::construct_runtime!(
|
||||
pub enum Test where
|
||||
Block = Block,
|
||||
NodeBlock = Block,
|
||||
UncheckedExtrinsic = UncheckedExtrinsic,
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
|
||||
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
|
||||
}
|
||||
);
|
||||
|
||||
parameter_types! {
|
||||
pub BlockWeights: frame_system::limits::BlockWeights =
|
||||
frame_system::limits::BlockWeights::simple_max(
|
||||
frame_support::weights::Weight::from_parts(1024, u64::MAX),
|
||||
);
|
||||
pub static ExistentialDeposit: u64 = 0;
|
||||
}
|
||||
impl frame_system::Config for Test {
|
||||
type BaseCallFilter = frame_support::traits::Everything;
|
||||
type BlockWeights = BlockWeights;
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type Index = u64;
|
||||
type BlockNumber = u64;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Hash = H256;
|
||||
type Hashing = ::sp_runtime::traits::BlakeTwo256;
|
||||
type AccountId = u64;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = Header;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = ConstU64<250>;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = ();
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = frame_support::traits::ConstU32<16>;
|
||||
}
|
||||
|
||||
pub struct OnDustRemoval;
|
||||
impl OnUnbalanced<NegativeImbalance<Test>> for OnDustRemoval {
|
||||
fn on_nonzero_unbalanced(amount: NegativeImbalance<Test>) {
|
||||
assert_ok!(Balances::resolve_into_existing(&1, amount));
|
||||
}
|
||||
}
|
||||
|
||||
impl Config for Test {
|
||||
type Balance = u64;
|
||||
type DustRemoval = OnDustRemoval;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore =
|
||||
StorageMapShim<super::Account<Test>, system::Provider<Test>, u64, super::AccountData<u64>>;
|
||||
type MaxLocks = ConstU32<50>;
|
||||
type MaxReserves = ConstU32<2>;
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
pub struct ExtBuilder {
|
||||
existential_deposit: u64,
|
||||
}
|
||||
impl Default for ExtBuilder {
|
||||
fn default() -> Self {
|
||||
Self { existential_deposit: 1 }
|
||||
}
|
||||
}
|
||||
impl ExtBuilder {
|
||||
pub fn existential_deposit(mut self, existential_deposit: u64) -> Self {
|
||||
self.existential_deposit = existential_deposit;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_associated_consts(&self) {
|
||||
EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit);
|
||||
}
|
||||
|
||||
pub fn build(self) -> sp_io::TestExternalities {
|
||||
self.set_associated_consts();
|
||||
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
|
||||
pallet_balances::GenesisConfig::<Test> { balances: vec![] }
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
let mut ext = sp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| System::set_block_number(1));
|
||||
ext
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_dust_removal_tst1_should_work() {
|
||||
ExtBuilder::default().existential_deposit(100).build().execute_with(|| {
|
||||
// Verification of reentrancy in dust removal
|
||||
assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 1, 1000, 0));
|
||||
assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 2, 500, 0));
|
||||
|
||||
// In this transaction, account 2 free balance
|
||||
// drops below existential balance
|
||||
// and dust balance is removed from account 2
|
||||
assert_ok!(Balances::transfer(RawOrigin::Signed(2).into(), 3, 450));
|
||||
|
||||
// As expected dust balance is removed.
|
||||
assert_eq!(Balances::free_balance(&2), 0);
|
||||
|
||||
// As expected beneficiary account 3
|
||||
// received the transfered fund.
|
||||
assert_eq!(Balances::free_balance(&3), 450);
|
||||
|
||||
// Dust balance is deposited to account 1
|
||||
// during the process of dust removal.
|
||||
assert_eq!(Balances::free_balance(&1), 1050);
|
||||
|
||||
// Verify the events
|
||||
assert_eq!(System::events().len(), 12);
|
||||
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer {
|
||||
from: 2,
|
||||
to: 3,
|
||||
amount: 450,
|
||||
}));
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost {
|
||||
account: 2,
|
||||
amount: 50,
|
||||
}));
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit {
|
||||
who: 1,
|
||||
amount: 50,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_dust_removal_tst2_should_work() {
|
||||
ExtBuilder::default().existential_deposit(100).build().execute_with(|| {
|
||||
// Verification of reentrancy in dust removal
|
||||
assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 1, 1000, 0));
|
||||
assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 2, 500, 0));
|
||||
|
||||
// In this transaction, account 2 free balance
|
||||
// drops below existential balance
|
||||
// and dust balance is removed from account 2
|
||||
assert_ok!(Balances::transfer(RawOrigin::Signed(2).into(), 1, 450));
|
||||
|
||||
// As expected dust balance is removed.
|
||||
assert_eq!(Balances::free_balance(&2), 0);
|
||||
|
||||
// Dust balance is deposited to account 1
|
||||
// during the process of dust removal.
|
||||
assert_eq!(Balances::free_balance(&1), 1500);
|
||||
|
||||
// Verify the events
|
||||
assert_eq!(System::events().len(), 10);
|
||||
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer {
|
||||
from: 2,
|
||||
to: 1,
|
||||
amount: 450,
|
||||
}));
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost {
|
||||
account: 2,
|
||||
amount: 50,
|
||||
}));
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit {
|
||||
who: 1,
|
||||
amount: 50,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repatriating_reserved_balance_dust_removal_should_work() {
|
||||
ExtBuilder::default().existential_deposit(100).build().execute_with(|| {
|
||||
// Verification of reentrancy in dust removal
|
||||
assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 1, 1000, 0));
|
||||
assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 2, 500, 0));
|
||||
|
||||
// Reserve a value on account 2,
|
||||
// Such that free balance is lower than
|
||||
// Exestintial deposit.
|
||||
assert_ok!(Balances::reserve(&2, 450));
|
||||
|
||||
// Transfer of reserved fund from slashed account 2 to
|
||||
// beneficiary account 1
|
||||
assert_ok!(Balances::repatriate_reserved(&2, &1, 450, Status::Free), 0);
|
||||
|
||||
// Since free balance of account 2 is lower than
|
||||
// existential deposit, dust amount is
|
||||
// removed from the account 2
|
||||
assert_eq!(Balances::reserved_balance(2), 0);
|
||||
assert_eq!(Balances::free_balance(2), 0);
|
||||
|
||||
// account 1 is credited with reserved amount
|
||||
// together with dust balance during dust
|
||||
// removal.
|
||||
assert_eq!(Balances::reserved_balance(1), 0);
|
||||
assert_eq!(Balances::free_balance(1), 1500);
|
||||
|
||||
// Verify the events
|
||||
assert_eq!(System::events().len(), 11);
|
||||
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::ReserveRepatriated {
|
||||
from: 2,
|
||||
to: 1,
|
||||
amount: 450,
|
||||
destination_status: Status::Free,
|
||||
}));
|
||||
|
||||
System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost {
|
||||
account: 2,
|
||||
amount: 50,
|
||||
}));
|
||||
|
||||
System::assert_last_event(RuntimeEvent::Balances(crate::Event::Deposit {
|
||||
who: 1,
|
||||
amount: 50,
|
||||
}));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Types used in the pallet.
|
||||
|
||||
use crate::{Config, CreditOf, Event, Pallet};
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use core::ops::BitOr;
|
||||
use frame_support::{
|
||||
traits::{Imbalance, LockIdentifier, OnUnbalanced, WithdrawReasons},
|
||||
RuntimeDebug,
|
||||
};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_runtime::Saturating;
|
||||
|
||||
/// Simplified reasons for withdrawing balance.
|
||||
#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
|
||||
pub enum Reasons {
|
||||
/// Paying system transaction fees.
|
||||
Fee = 0,
|
||||
/// Any reason other than paying system transaction fees.
|
||||
Misc = 1,
|
||||
/// Any reason at all.
|
||||
All = 2,
|
||||
}
|
||||
|
||||
impl From<WithdrawReasons> for Reasons {
|
||||
fn from(r: WithdrawReasons) -> Reasons {
|
||||
if r == WithdrawReasons::TRANSACTION_PAYMENT {
|
||||
Reasons::Fee
|
||||
} else if r.contains(WithdrawReasons::TRANSACTION_PAYMENT) {
|
||||
Reasons::All
|
||||
} else {
|
||||
Reasons::Misc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOr for Reasons {
|
||||
type Output = Reasons;
|
||||
fn bitor(self, other: Reasons) -> Reasons {
|
||||
if self == other {
|
||||
return self
|
||||
}
|
||||
Reasons::All
|
||||
}
|
||||
}
|
||||
|
||||
/// 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, RuntimeDebug, MaxEncodedLen, TypeInfo)]
|
||||
pub struct BalanceLock<Balance> {
|
||||
/// An identifier for this lock. Only one lock may be in existence for each identifier.
|
||||
pub id: LockIdentifier,
|
||||
/// The amount which the free balance may not drop below when this lock is in effect.
|
||||
pub amount: Balance,
|
||||
/// If true, then the lock remains in effect even for payment of transaction fees.
|
||||
pub reasons: Reasons,
|
||||
}
|
||||
|
||||
/// Store named reserved balance.
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
|
||||
pub struct ReserveData<ReserveIdentifier, Balance> {
|
||||
/// The identifier for the named reserve.
|
||||
pub id: ReserveIdentifier,
|
||||
/// The amount of the named reserve.
|
||||
pub amount: Balance,
|
||||
}
|
||||
|
||||
/// An identifier and balance.
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
|
||||
pub struct IdAmount<Id, Balance> {
|
||||
/// An identifier for this item.
|
||||
pub id: Id,
|
||||
/// Some amount for this item.
|
||||
pub amount: Balance,
|
||||
}
|
||||
|
||||
/// All balance information for an account.
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, Default, RuntimeDebug, MaxEncodedLen, TypeInfo)]
|
||||
pub struct AccountData<Balance> {
|
||||
/// Non-reserved part of the balance which the account holder may be able to control.
|
||||
///
|
||||
/// This is the only balance that matters in terms of most operations on tokens.
|
||||
pub free: Balance,
|
||||
/// Balance which is has active holds on it and may not be used at all.
|
||||
///
|
||||
/// This is the sum of all individual holds together with any sums still under the (deprecated)
|
||||
/// reserves API.
|
||||
pub reserved: Balance,
|
||||
/// The amount that `free` may not drop below when reducing the balance, except for actions
|
||||
/// where the account owner cannot reasonably benefit from thr balance reduction, such as
|
||||
/// slashing.
|
||||
pub frozen: Balance,
|
||||
/// Extra information about this account. The MSB is a flag indicating whether the new ref-
|
||||
/// counting logic is in place for this account.
|
||||
pub flags: ExtraFlags,
|
||||
}
|
||||
|
||||
const IS_NEW_LOGIC: u128 = 0x80000000_00000000_00000000_00000000u128;
|
||||
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
|
||||
pub struct ExtraFlags(u128);
|
||||
impl Default for ExtraFlags {
|
||||
fn default() -> Self {
|
||||
Self(IS_NEW_LOGIC)
|
||||
}
|
||||
}
|
||||
impl ExtraFlags {
|
||||
pub fn old_logic() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
pub fn set_new_logic(&mut self) {
|
||||
self.0 = self.0 | IS_NEW_LOGIC
|
||||
}
|
||||
pub fn is_new_logic(&self) -> bool {
|
||||
(self.0 & IS_NEW_LOGIC) == IS_NEW_LOGIC
|
||||
}
|
||||
}
|
||||
|
||||
impl<Balance: Saturating + Copy + Ord> AccountData<Balance> {
|
||||
pub 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 fn total(&self) -> Balance {
|
||||
self.free.saturating_add(self.reserved)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DustCleaner<T: Config<I>, I: 'static = ()>(
|
||||
pub(crate) Option<(T::AccountId, CreditOf<T, I>)>,
|
||||
);
|
||||
|
||||
impl<T: Config<I>, I: 'static> Drop for DustCleaner<T, I> {
|
||||
fn drop(&mut self) {
|
||||
if let Some((who, dust)) = self.0.take() {
|
||||
Pallet::<T, I>::deposit_event(Event::DustLost { account: who, amount: dust.peek() });
|
||||
T::DustRemoval::on_unbalanced(dust);
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+40
-9
@@ -49,13 +49,14 @@ use sp_std::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for pallet_balances.
|
||||
pub trait WeightInfo {
|
||||
fn transfer() -> Weight;
|
||||
fn transfer_allow_death() -> Weight;
|
||||
fn transfer_keep_alive() -> Weight;
|
||||
fn set_balance_creating() -> Weight;
|
||||
fn set_balance_killing() -> Weight;
|
||||
fn force_set_balance_creating() -> Weight;
|
||||
fn force_set_balance_killing() -> Weight;
|
||||
fn force_transfer() -> Weight;
|
||||
fn transfer_all() -> Weight;
|
||||
fn force_unreserve() -> Weight;
|
||||
fn upgrade_accounts(u: u32, ) -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for pallet_balances using the Substrate node and recommended hardware.
|
||||
@@ -63,7 +64,7 @@ pub struct SubstrateWeight<T>(PhantomData<T>);
|
||||
impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
/// Storage: System Account (r:1 w:1)
|
||||
/// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen)
|
||||
fn transfer() -> Weight {
|
||||
fn transfer_allow_death() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `0`
|
||||
// Estimated: `3593`
|
||||
@@ -85,7 +86,7 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
}
|
||||
/// Storage: System Account (r:1 w:1)
|
||||
/// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen)
|
||||
fn set_balance_creating() -> Weight {
|
||||
fn force_set_balance_creating() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `174`
|
||||
// Estimated: `3593`
|
||||
@@ -96,7 +97,7 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
}
|
||||
/// Storage: System Account (r:1 w:1)
|
||||
/// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen)
|
||||
fn set_balance_killing() -> Weight {
|
||||
fn force_set_balance_killing() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `174`
|
||||
// Estimated: `3593`
|
||||
@@ -138,13 +139,28 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: System Account (r:999 w:999)
|
||||
/// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen)
|
||||
/// The range of component `u` is `[1, 1000]`.
|
||||
fn upgrade_accounts(u: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `0 + u * (135 ±0)`
|
||||
// Estimated: `990 + u * (2603 ±0)`
|
||||
// Minimum execution time: 19_851_000 picoseconds.
|
||||
Weight::from_parts(20_099_000, 990)
|
||||
// Standard Error: 15_586
|
||||
.saturating_add(Weight::from_parts(14_892_860, 0).saturating_mul(u.into()))
|
||||
.saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(u.into())))
|
||||
.saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(u.into())))
|
||||
.saturating_add(Weight::from_parts(0, 2603).saturating_mul(u.into()))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests
|
||||
impl WeightInfo for () {
|
||||
/// Storage: System Account (r:1 w:1)
|
||||
/// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen)
|
||||
fn transfer() -> Weight {
|
||||
fn transfer_allow_death() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `0`
|
||||
// Estimated: `3593`
|
||||
@@ -166,7 +182,7 @@ impl WeightInfo for () {
|
||||
}
|
||||
/// Storage: System Account (r:1 w:1)
|
||||
/// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen)
|
||||
fn set_balance_creating() -> Weight {
|
||||
fn force_set_balance_creating() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `174`
|
||||
// Estimated: `3593`
|
||||
@@ -177,7 +193,7 @@ impl WeightInfo for () {
|
||||
}
|
||||
/// Storage: System Account (r:1 w:1)
|
||||
/// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen)
|
||||
fn set_balance_killing() -> Weight {
|
||||
fn force_set_balance_killing() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `174`
|
||||
// Estimated: `3593`
|
||||
@@ -219,4 +235,19 @@ impl WeightInfo for () {
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: System Account (r:999 w:999)
|
||||
/// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen)
|
||||
/// The range of component `u` is `[1, 1000]`.
|
||||
fn upgrade_accounts(u: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `0 + u * (135 ±0)`
|
||||
// Estimated: `990 + u * (2603 ±0)`
|
||||
// Minimum execution time: 19_851_000 picoseconds.
|
||||
Weight::from_parts(20_099_000, 990)
|
||||
// Standard Error: 15_586
|
||||
.saturating_add(Weight::from_parts(14_892_860, 0).saturating_mul(u.into()))
|
||||
.saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(u.into())))
|
||||
.saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(u.into())))
|
||||
.saturating_add(Weight::from_parts(0, 2603).saturating_mul(u.into()))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user