From e5c876dd6b59e2b7dbacaa4538cb42c802db3730 Mon Sep 17 00:00:00 2001 From: Liam Aharon <liam.aharon@hotmail.com> Date: Wed, 30 Aug 2023 13:40:20 +1000 Subject: [PATCH] balanced and unbalanced conformance tests --- substrate/frame/balances/src/impl_fungible.rs | 5 +- .../src/tests/fungible_conformance_tests.rs | 81 +- .../tokens/fungible/conformance_tests/mod.rs | 2 +- .../fungible/conformance_tests/regular.rs | 1347 +++++++++++++++++ .../src/traits/tokens/fungible/item_of.rs | 2 +- .../src/traits/tokens/fungible/regular.rs | 76 +- 6 files changed, 1461 insertions(+), 52 deletions(-) create mode 100644 substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs diff --git a/substrate/frame/balances/src/impl_fungible.rs b/substrate/frame/balances/src/impl_fungible.rs index 03c40bb3a84..01cb64e66eb 100644 --- a/substrate/frame/balances/src/impl_fungible.rs +++ b/substrate/frame/balances/src/impl_fungible.rs @@ -174,7 +174,10 @@ impl<T: Config<I>, I: 'static> fungible::Unbalanced<T::AccountId> for Pallet<T, } fn deactivate(amount: Self::Balance) { - InactiveIssuance::<T, I>::mutate(|b| b.saturating_accrue(amount)); + InactiveIssuance::<T, I>::mutate(|b| { + // InactiveIssuance cannot be greater than TotalIssuance. + *b = b.saturating_add(amount).min(TotalIssuance::<T, I>::get()); + }); } fn reactivate(amount: Self::Balance) { diff --git a/substrate/frame/balances/src/tests/fungible_conformance_tests.rs b/substrate/frame/balances/src/tests/fungible_conformance_tests.rs index 6262aa04dc0..5c0c19a554a 100644 --- a/substrate/frame/balances/src/tests/fungible_conformance_tests.rs +++ b/substrate/frame/balances/src/tests/fungible_conformance_tests.rs @@ -19,17 +19,19 @@ use super::*; use frame_support::traits::fungible::{conformance_tests, Inspect, Mutate}; use paste::paste; -macro_rules! run_tests { - ($path:path, $ext_deposit:expr, $($name:ident),*) => { +macro_rules! generate_tests { + // Handle a conformance test that requires special testing with and without a dust trap. + (dust_trap_variation, $base_path:path, $scope:expr, $trait:ident, $ext_deposit:expr, $($test_name:ident),*) => { $( paste! { #[test] - fn [< $name _existential_deposit_ $ext_deposit _dust_trap_on >]() { + fn [<$trait _ $scope _ $test_name _existential_deposit_ $ext_deposit _dust_trap_on >]() { + // Some random trap account. let trap_account = <Test as frame_system::Config>::AccountId::from(65174286u64); let builder = ExtBuilder::default().existential_deposit($ext_deposit).dust_trap(trap_account); builder.build_and_execute_with(|| { Balances::set_balance(&trap_account, Balances::minimum_balance()); - $path::$name::< + $base_path::$scope::$trait::$test_name::< Balances, <Test as frame_system::Config>::AccountId, >(Some(trap_account)); @@ -37,10 +39,10 @@ macro_rules! run_tests { } #[test] - fn [< $name _existential_deposit_ $ext_deposit _dust_trap_off >]() { + fn [< $trait _ $scope _ $test_name _existential_deposit_ $ext_deposit _dust_trap_off >]() { let builder = ExtBuilder::default().existential_deposit($ext_deposit); builder.build_and_execute_with(|| { - $path::$name::< + $base_path::$scope::$trait::$test_name::< Balances, <Test as frame_system::Config>::AccountId, >(None); @@ -49,9 +51,37 @@ macro_rules! run_tests { } )* }; - ($path:path, $ext_deposit:expr) => { - run_tests!( - $path, + // Regular conformance test + ($base_path:path, $scope:expr, $trait:ident, $ext_deposit:expr, $($test_name:ident),*) => { + $( + paste! { + #[test] + fn [< $trait _ $scope _ $test_name _existential_deposit_ $ext_deposit>]() { + let builder = ExtBuilder::default().existential_deposit($ext_deposit); + builder.build_and_execute_with(|| { + $base_path::$scope::$trait::$test_name::< + Balances, + <Test as frame_system::Config>::AccountId, + >(); + }); + } + } + )* + }; + ($base_path:path, $ext_deposit:expr) => { + // regular::mutate + generate_tests!( + dust_trap_variation, + $base_path, + regular, + mutate, + $ext_deposit, + transfer_expendable_dust + ); + generate_tests!( + $base_path, + regular, + mutate, $ext_deposit, mint_into_success, mint_into_overflow, @@ -66,7 +96,6 @@ macro_rules! run_tests { shelve_insufficient_funds, transfer_success, transfer_expendable_all, - transfer_expendable_dust, transfer_protect_preserve, set_balance_mint_success, set_balance_burn_success, @@ -79,10 +108,34 @@ macro_rules! run_tests { reducible_balance_expendable, reducible_balance_protect_preserve ); + // regular::unbalanced + generate_tests!( + $base_path, + regular, + unbalanced, + $ext_deposit, + write_balance, + decrease_balance_expendable, + decrease_balance_preserve, + increase_balance, + set_total_issuance, + deactivate_and_reactivate + ); + // regular::balanced + generate_tests!( + $base_path, + regular, + balanced, + $ext_deposit, + issue_and_resolve_credit, + rescind_and_settle_debt, + deposit, + withdraw, + pair + ); }; } -run_tests!(conformance_tests::inspect_mutate, 1); -run_tests!(conformance_tests::inspect_mutate, 2); -run_tests!(conformance_tests::inspect_mutate, 5); -run_tests!(conformance_tests::inspect_mutate, 1000); +generate_tests!(conformance_tests, 1); +generate_tests!(conformance_tests, 5); +generate_tests!(conformance_tests, 1000); diff --git a/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/mod.rs b/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/mod.rs index 88ba56a6fed..22942e9a6d7 100644 --- a/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/mod.rs +++ b/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/mod.rs @@ -1 +1 @@ -pub mod inspect_mutate; +pub mod regular; diff --git a/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs b/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs new file mode 100644 index 00000000000..4401a6d4260 --- /dev/null +++ b/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs @@ -0,0 +1,1347 @@ +// 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. + +pub mod mutate { + use crate::traits::{ + fungible::{Inspect, Mutate}, + tokens::{ + DepositConsequence, Fortitude, Precision, Preservation, Provenance, WithdrawConsequence, + }, + }; + use core::fmt::Debug; + use sp_arithmetic::traits::AtLeast8BitUnsigned; + use sp_runtime::traits::{Bounded, Zero}; + + /// Test [`Mutate::mint_into`] for successful token minting. + /// + /// It ensures that account balances and total issuance values are updated correctly after + /// minting tokens into two distinct accounts. + pub fn mint_into_success<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + let account_0 = AccountId::from(0); + let account_1 = AccountId::from(1); + + // Test: Mint an amount into each account + let amount_0 = T::minimum_balance(); + let amount_1 = T::minimum_balance() + 5.into(); + T::mint_into(&account_0, amount_0).unwrap(); + T::mint_into(&account_1, amount_1).unwrap(); + + // Verify: Account balances are updated correctly + assert_eq!(T::total_balance(&account_0), amount_0); + assert_eq!(T::total_balance(&account_1), amount_1); + assert_eq!(T::balance(&account_0), amount_0); + assert_eq!(T::balance(&account_1), amount_1); + + // Verify: Total issuance is updated correctly + assert_eq!(T::total_issuance(), initial_total_issuance + amount_0 + amount_1); + assert_eq!(T::active_issuance(), initial_active_issuance + amount_0 + amount_1); + } + + /// Test [`Mutate::mint_into`] for overflow prevention. + /// + /// This test ensures that minting tokens beyond the maximum balance value for an account + /// returns an error and does not change the account balance or total issuance values. + pub fn mint_into_overflow<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + let account = AccountId::from(10); + let amount = T::Balance::max_value() - 5.into() - initial_total_issuance; + + // Mint just below the maximum balance + T::mint_into(&account, amount).unwrap(); + + // Verify: Minting beyond the maximum balance value returns an Err + T::mint_into(&account, 10.into()).unwrap_err(); + + // Verify: The balance did not change + assert_eq!(T::total_balance(&account), amount); + assert_eq!(T::balance(&account), amount); + + // Verify: The total issuance did not change + assert_eq!(T::total_issuance(), initial_total_issuance + amount); + assert_eq!(T::active_issuance(), initial_active_issuance + amount); + } + + /// Test [`Mutate::mint_into`] for handling balances below the minimum value. + /// + /// This test verifies that minting tokens below the minimum balance for an account + /// returns an error and has no impact on the account balance or total issuance values. + pub fn mint_into_below_minimum<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + // Skip if there is no minimum balance + if T::minimum_balance() == T::Balance::zero() { + return + } + + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + let account = AccountId::from(10); + let amount = T::minimum_balance() - 1.into(); + + // Verify: Minting below the minimum balance returns Err + T::mint_into(&account, amount).unwrap_err(); + + // Verify: noop + assert_eq!(T::total_balance(&account), T::Balance::zero()); + assert_eq!(T::balance(&account), T::Balance::zero()); + assert_eq!(T::total_issuance(), initial_total_issuance); + assert_eq!(T::active_issuance(), initial_active_issuance); + } + + /// Test [`Mutate::burn_from`] for successfully burning an exact amount of tokens. + /// + /// This test checks that burning tokens with [`Precision::Exact`] correctly reduces the account + /// balance and total issuance values by the burned amount. + pub fn burn_from_exact_success<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + + // Setup account + let account = AccountId::from(5); + let initial_balance = T::minimum_balance() + 10.into(); + T::mint_into(&account, initial_balance).unwrap(); + + // Test: Burn an exact amount from the account + let amount_to_burn = T::Balance::from(5); + let precision = Precision::Exact; + let force = Fortitude::Polite; + T::burn_from(&account, amount_to_burn, precision, force).unwrap(); + + // Verify: The balance and total issuance should be reduced by the burned amount + assert_eq!(T::balance(&account), initial_balance - amount_to_burn); + assert_eq!(T::total_balance(&account), initial_balance - amount_to_burn); + assert_eq!(T::total_issuance(), initial_total_issuance + initial_balance - amount_to_burn); + assert_eq!( + T::active_issuance(), + initial_active_issuance + initial_balance - amount_to_burn + ); + } + + /// Test [`Mutate::burn_from`] for successfully burning tokens with [`Precision::BestEffort`]. + /// + /// This test verifies that the burning tokens with best-effort precision correctly reduces the + /// account balance and total issuance values by the reducible balance when attempting to burn + /// an amount greater than the reducible balance. + pub fn burn_from_best_effort_success<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + + // Setup account + let account = AccountId::from(5); + let initial_balance = T::minimum_balance() + 10.into(); + T::mint_into(&account, initial_balance).unwrap(); + + // Get reducible balance + let force = Fortitude::Polite; + let reducible_balance = T::reducible_balance(&account, Preservation::Expendable, force); + + // Test: Burn a best effort amount from the account that is greater than the reducible + // balance + let amount_to_burn = reducible_balance + 5.into(); + let precision = Precision::BestEffort; + assert!(amount_to_burn > reducible_balance); + assert!(amount_to_burn > T::balance(&account)); + T::burn_from(&account, amount_to_burn, precision, force).unwrap(); + + // Verify: The balance and total issuance should be reduced by the reducible_balance + assert_eq!(T::balance(&account), initial_balance - reducible_balance); + assert_eq!(T::total_balance(&account), initial_balance - reducible_balance); + assert_eq!( + T::total_issuance(), + initial_total_issuance + initial_balance - reducible_balance + ); + assert_eq!( + T::active_issuance(), + initial_active_issuance + initial_balance - reducible_balance + ); + } + + /// Test [`Mutate::burn_from`] handling of insufficient funds when called with + /// [`Precision::Exact`]. + /// + /// This test verifies that burning an amount greater than the account's balance with exact + /// precision returns an error and does not change the account balance or total issuance values. + pub fn burn_from_exact_insufficient_funds<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + // Set up the initial conditions and parameters for the test + let account = AccountId::from(5); + let initial_balance = T::minimum_balance() + 10.into(); + T::mint_into(&account, initial_balance).unwrap(); + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + + // Verify: Burn an amount greater than the account's balance with Exact precision returns + // Err + let amount_to_burn = initial_balance + 10.into(); + let precision = Precision::Exact; + let force = Fortitude::Polite; + T::burn_from(&account, amount_to_burn, precision, force).unwrap_err(); + + // Verify: The balance and total issuance should remain unchanged + assert_eq!(T::balance(&account), initial_balance); + assert_eq!(T::total_balance(&account), initial_balance); + assert_eq!(T::total_issuance(), initial_total_issuance); + assert_eq!(T::active_issuance(), initial_active_issuance); + } + + /// Test [`Mutate::restore`] for successful restoration. + /// + /// This test verifies that restoring an amount into each account updates their balances and the + /// total issuance values correctly. + pub fn restore_success<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let account_0 = AccountId::from(0); + let account_1 = AccountId::from(1); + + // Test: Restore an amount into each account + let amount_0 = T::minimum_balance(); + let amount_1 = T::minimum_balance() + 5.into(); + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + T::restore(&account_0, amount_0).unwrap(); + T::restore(&account_1, amount_1).unwrap(); + + // Verify: Account balances are updated correctly + assert_eq!(T::total_balance(&account_0), amount_0); + assert_eq!(T::total_balance(&account_1), amount_1); + assert_eq!(T::balance(&account_0), amount_0); + assert_eq!(T::balance(&account_1), amount_1); + + // Verify: Total issuance is updated correctly + assert_eq!(T::total_issuance(), initial_total_issuance + amount_0 + amount_1); + assert_eq!(T::active_issuance(), initial_active_issuance + amount_0 + amount_1); + } + + /// Test [`Mutate::restore`] handles balance overflow. + /// + /// This test verifies that restoring an amount beyond the maximum balance returns an error and + /// does not change the account balance or total issuance values. + pub fn restore_overflow<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + let account = AccountId::from(10); + let amount = T::Balance::max_value() - 5.into() - initial_total_issuance; + + // Restore just below the maximum balance + T::restore(&account, amount).unwrap(); + + // Verify: Restoring beyond the maximum balance returns an Err + T::restore(&account, 10.into()).unwrap_err(); + + // Verify: The balance and total issuance did not change + assert_eq!(T::total_balance(&account), amount); + assert_eq!(T::balance(&account), amount); + assert_eq!(T::total_issuance(), initial_total_issuance + amount); + assert_eq!(T::active_issuance(), initial_active_issuance + amount); + } + + /// Test [`Mutate::restore`] handles restoration below the minimum balance. + /// + /// This test verifies that restoring an amount below the minimum balance returns an error and + /// does not change the account balance or total issuance values. + pub fn restore_below_minimum<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + // Skip if there is no minimum balance + if T::minimum_balance() == T::Balance::zero() { + return + } + + let account = AccountId::from(10); + let amount = T::minimum_balance() - 1.into(); + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + + // Verify: Restoring below the minimum balance returns Err + T::restore(&account, amount).unwrap_err(); + + // Verify: noop + assert_eq!(T::total_balance(&account), T::Balance::zero()); + assert_eq!(T::balance(&account), T::Balance::zero()); + assert_eq!(T::total_issuance(), initial_total_issuance); + assert_eq!(T::active_issuance(), initial_active_issuance); + } + + /// Test [`Mutate::shelve`] for successful shelving. + /// + /// This test verifies that shelving an amount from an account reduces the account balance and + /// total issuance values by the shelved amount. + pub fn shelve_success<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + + // Setup account + let account = AccountId::from(5); + let initial_balance = T::minimum_balance() + 10.into(); + + T::restore(&account, initial_balance).unwrap(); + + // Test: Shelve an amount from the account + let amount_to_shelve = T::Balance::from(5); + T::shelve(&account, amount_to_shelve).unwrap(); + + // Verify: The balance and total issuance should be reduced by the shelved amount + assert_eq!(T::balance(&account), initial_balance - amount_to_shelve); + assert_eq!(T::total_balance(&account), initial_balance - amount_to_shelve); + assert_eq!( + T::total_issuance(), + initial_total_issuance + initial_balance - amount_to_shelve + ); + assert_eq!( + T::active_issuance(), + initial_active_issuance + initial_balance - amount_to_shelve + ); + } + + /// Test [`Mutate::shelve`] handles insufficient funds correctly. + /// + /// This test verifies that attempting to shelve an amount greater than the account's balance + /// returns an error and does not change the account balance or total issuance values. + pub fn shelve_insufficient_funds<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + + // Set up the initial conditions and parameters for the test + let account = AccountId::from(5); + let initial_balance = T::minimum_balance() + 10.into(); + T::restore(&account, initial_balance).unwrap(); + + // Verify: Shelving greater than the balance with Exact precision returns Err + let amount_to_shelve = initial_balance + 10.into(); + T::shelve(&account, amount_to_shelve).unwrap_err(); + + // Verify: The balance and total issuance should remain unchanged + assert_eq!(T::balance(&account), initial_balance); + assert_eq!(T::total_balance(&account), initial_balance); + assert_eq!(T::total_issuance(), initial_total_issuance + initial_balance); + assert_eq!(T::active_issuance(), initial_active_issuance + initial_balance); + } + + /// Test [`Mutate::transfer`] for a successful transfer. + /// + /// This test verifies that transferring an amount between two accounts with updates the account + /// balances and maintains correct total issuance and active issuance values. + pub fn transfer_success<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + let account_0 = AccountId::from(0); + let account_1 = AccountId::from(1); + let initial_balance = T::minimum_balance() + 10.into(); + T::set_balance(&account_0, initial_balance); + T::set_balance(&account_1, initial_balance); + + // Test: Transfer an amount from account_0 to account_1 + let transfer_amount = T::Balance::from(3); + T::transfer(&account_0, &account_1, transfer_amount, Preservation::Expendable).unwrap(); + + // Verify: Account balances are updated correctly + assert_eq!(T::total_balance(&account_0), initial_balance - transfer_amount); + assert_eq!(T::total_balance(&account_1), initial_balance + transfer_amount); + assert_eq!(T::balance(&account_0), initial_balance - transfer_amount); + assert_eq!(T::balance(&account_1), initial_balance + transfer_amount); + + // Verify: Total issuance doesn't change + assert_eq!(T::total_issuance(), initial_total_issuance + initial_balance * 2.into()); + assert_eq!(T::active_issuance(), initial_active_issuance + initial_balance * 2.into()); + } + + /// Test calling [`Mutate::transfer`] with [`Preservation::Expendable`] correctly transfers the + /// entire balance. + /// + /// This test verifies that transferring the entire balance from one account to another with + /// when preservation is expendable updates the account balances and maintains the total + /// issuance and active issuance values. + pub fn transfer_expendable_all<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + let account_0 = AccountId::from(0); + let account_1 = AccountId::from(1); + let initial_balance = T::minimum_balance() + 10.into(); + T::set_balance(&account_0, initial_balance); + T::set_balance(&account_1, initial_balance); + + // Test: Transfer entire balance from account_0 to account_1 + let preservation = Preservation::Expendable; + let transfer_amount = initial_balance; + T::transfer(&account_0, &account_1, transfer_amount, preservation).unwrap(); + + // Verify: Account balances are updated correctly + assert_eq!(T::total_balance(&account_0), T::Balance::zero()); + assert_eq!(T::total_balance(&account_1), initial_balance * 2.into()); + assert_eq!(T::balance(&account_0), T::Balance::zero()); + assert_eq!(T::balance(&account_1), initial_balance * 2.into()); + + // Verify: Total issuance doesn't change + assert_eq!(T::total_issuance(), initial_total_issuance + initial_balance * 2.into()); + assert_eq!(T::active_issuance(), initial_active_issuance + initial_balance * 2.into()); + } + + /// Test calling [`Mutate::transfer`] function with [`Preservation::Expendable`] and an amount + /// that results in some dust. + /// + /// This test verifies that dust is handled correctly when an account is reaped, with and + /// without a dust trap. + /// + /// # Parameters + /// + /// - dust_trap: An optional account identifier to which dust will be collected. If `None`, dust + /// is expected to be removed from the total and active issuance. + pub fn transfer_expendable_dust<T, AccountId>(dust_trap: Option<AccountId>) + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + if T::minimum_balance() == T::Balance::zero() { + return + } + + let account_0 = AccountId::from(10); + let account_1 = AccountId::from(20); + let initial_balance = T::minimum_balance() + 10.into(); + T::set_balance(&account_0, initial_balance); + T::set_balance(&account_1, initial_balance); + + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + let initial_dust_trap_balance = match dust_trap.clone() { + Some(dust_trap) => T::total_balance(&dust_trap), + None => T::Balance::zero(), + }; + + // Test: Transfer balance + let preservation = Preservation::Expendable; + let transfer_amount = T::Balance::from(11); + T::transfer(&account_0, &account_1, transfer_amount, preservation).unwrap(); + + // Verify: Account balances are updated correctly + assert_eq!(T::total_balance(&account_0), T::Balance::zero()); + assert_eq!(T::total_balance(&account_1), initial_balance + transfer_amount); + assert_eq!(T::balance(&account_0), T::Balance::zero()); + assert_eq!(T::balance(&account_1), initial_balance + transfer_amount); + + match dust_trap { + Some(dust_trap) => { + // Verify: Total issuance and active issuance don't change + assert_eq!(T::total_issuance(), initial_total_issuance); + assert_eq!(T::active_issuance(), initial_active_issuance); + // Verify: Dust is collected into dust trap + assert_eq!( + T::total_balance(&dust_trap), + initial_dust_trap_balance + T::minimum_balance() - 1.into() + ); + assert_eq!( + T::balance(&dust_trap), + initial_dust_trap_balance + T::minimum_balance() - 1.into() + ); + }, + None => { + // Verify: Total issuance and active issuance are reduced by the dust amount + assert_eq!( + T::total_issuance(), + initial_total_issuance - T::minimum_balance() + 1.into() + ); + assert_eq!( + T::active_issuance(), + initial_active_issuance - T::minimum_balance() + 1.into() + ); + }, + } + } + + /// Test [`Mutate::transfer`] with [`Preservation::Protect`] and [`Preservation::Preserve`] + /// transferring the entire balance. + /// + /// This test verifies that attempting to transfer the entire balance with returns an error when + /// preservation should not allow it, and the account balances, total issuance, and active + /// issuance values remain unchanged. + pub fn transfer_protect_preserve<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + // This test means nothing if there is no minimum balance + if T::minimum_balance() == T::Balance::zero() { + return + } + + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + let account_0 = AccountId::from(0); + let account_1 = AccountId::from(1); + let initial_balance = T::minimum_balance() + 10.into(); + T::set_balance(&account_0, initial_balance); + T::set_balance(&account_1, initial_balance); + + // Verify: Transfer Protect entire balance from account_0 to account_1 should Err + let preservation = Preservation::Protect; + let transfer_amount = initial_balance; + T::transfer(&account_0, &account_1, transfer_amount, preservation).unwrap_err(); + + // Verify: Noop + assert_eq!(T::total_balance(&account_0), initial_balance); + assert_eq!(T::total_balance(&account_1), initial_balance); + assert_eq!(T::balance(&account_0), initial_balance); + assert_eq!(T::balance(&account_1), initial_balance); + assert_eq!(T::total_issuance(), initial_total_issuance + initial_balance * 2.into()); + assert_eq!(T::active_issuance(), initial_active_issuance + initial_balance * 2.into()); + + // Verify: Transfer Preserve entire balance from account_0 to account_1 should Err + let preservation = Preservation::Preserve; + T::transfer(&account_0, &account_1, transfer_amount, preservation).unwrap_err(); + + // Verify: Noop + assert_eq!(T::total_balance(&account_0), initial_balance); + assert_eq!(T::total_balance(&account_1), initial_balance); + assert_eq!(T::balance(&account_0), initial_balance); + assert_eq!(T::balance(&account_1), initial_balance); + assert_eq!(T::total_issuance(), initial_total_issuance + initial_balance * 2.into()); + assert_eq!(T::active_issuance(), initial_active_issuance + initial_balance * 2.into()); + } + + /// Test [`Mutate::set_balance`] mints balances correctly. + /// + /// This test verifies that minting a balance using `set_balance` updates the account balance, + /// total issuance, and active issuance correctly. + pub fn set_balance_mint_success<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + let account = AccountId::from(10); + let initial_balance = T::minimum_balance() + 10.into(); + T::mint_into(&account, initial_balance).unwrap(); + + // Test: Increase the account balance with set_balance + let increase_amount: T::Balance = 5.into(); + let new = T::set_balance(&account, initial_balance + increase_amount); + + // Verify: set_balance returned the new balance + let expected_new = initial_balance + increase_amount; + assert_eq!(new, expected_new); + + // Verify: Balance and issuance is updated correctly + assert_eq!(T::total_balance(&account), expected_new); + assert_eq!(T::balance(&account), expected_new); + assert_eq!(T::total_issuance(), initial_total_issuance + expected_new); + assert_eq!(T::active_issuance(), initial_active_issuance + expected_new); + } + + /// Test [`Mutate::set_balance`] burns balances correctly. + /// + /// This test verifies that burning a balance using `set_balance` updates the account balance, + /// total issuance, and active issuance correctly. + pub fn set_balance_burn_success<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let initial_total_issuance = T::total_issuance(); + let initial_active_issuance = T::active_issuance(); + let account = AccountId::from(10); + let initial_balance = T::minimum_balance() + 10.into(); + T::mint_into(&account, initial_balance).unwrap(); + + // Test: Increase the account balance with set_balance + let burn_amount: T::Balance = 5.into(); + let new = T::set_balance(&account, initial_balance - burn_amount); + + // Verify: set_balance returned the new balance + let expected_new = initial_balance - burn_amount; + assert_eq!(new, expected_new); + + // Verify: Balance and issuance is updated correctly + assert_eq!(T::total_balance(&account), expected_new); + assert_eq!(T::balance(&account), expected_new); + assert_eq!(T::total_issuance(), initial_total_issuance + expected_new); + assert_eq!(T::active_issuance(), initial_active_issuance + expected_new); + } + + /// Test [`Inspect::can_deposit`] works correctly returns [`DepositConsequence::Success`] + /// when depositing an amount that should succeed. + pub fn can_deposit_success<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let account = AccountId::from(10); + let initial_balance = T::minimum_balance() + 10.into(); + T::mint_into(&account, initial_balance).unwrap(); + + // Test: can_deposit a reasonable amount + let ret = T::can_deposit(&account, 5.into(), Provenance::Minted); + + // Verify: Returns success + assert_eq!(ret, DepositConsequence::Success); + } + + /// Test [`Inspect::can_deposit`] returns [`DepositConsequence::BelowMinimum`] when depositing + /// below the minimum balance. + pub fn can_deposit_below_minimum<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + // can_deposit always returns Success for amount 0 + if T::minimum_balance() < 2.into() { + return + } + + let account = AccountId::from(10); + + // Test: can_deposit below the minimum + let ret = T::can_deposit(&account, T::minimum_balance() - 1.into(), Provenance::Minted); + + // Verify: Returns success + assert_eq!(ret, DepositConsequence::BelowMinimum); + } + + /// Test [`Inspect::can_deposit`] returns [`DepositConsequence::Overflow`] when + /// depositing an amount that would overflow. + pub fn can_deposit_overflow<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let account = AccountId::from(10); + + // Test: Try deposit over the max balance + let initial_balance = T::Balance::max_value() - 5.into() - T::total_issuance(); + T::mint_into(&account, initial_balance).unwrap(); + let ret = T::can_deposit(&account, 10.into(), Provenance::Minted); + + // Verify: Returns success + assert_eq!(ret, DepositConsequence::Overflow); + } + + /// Test [`Inspect::can_withdraw`] returns [`WithdrawConsequence::Success`] when withdrawing an + /// amount that should succeed. + pub fn can_withdraw_success<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let account = AccountId::from(10); + let initial_balance = T::minimum_balance() + 10.into(); + T::mint_into(&account, initial_balance).unwrap(); + + // Test: can_withdraw a reasonable amount + let ret = T::can_withdraw(&account, 5.into()); + + // Verify: Returns success + assert_eq!(ret, WithdrawConsequence::Success); + } + + /// Test [`Inspect::can_withdraw`] returns [`WithdrawConsequence::ReducedToZero`] when + /// withdrawing an amount that would reduce the account balance below the minimum balance. + pub fn can_withdraw_reduced_to_zero<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + if T::minimum_balance() == T::Balance::zero() { + return + } + + let account = AccountId::from(10); + let initial_balance = T::minimum_balance(); + T::mint_into(&account, initial_balance).unwrap(); + + // Verify: can_withdraw below the minimum balance returns ReducedToZero + let ret = T::can_withdraw(&account, 1.into()); + assert_eq!(ret, WithdrawConsequence::ReducedToZero(T::minimum_balance() - 1.into())); + } + + /// Test [`Inspect::can_withdraw`] returns [`WithdrawConsequence::BalanceLow`] when withdrawing + /// an amount that would result in an account balance below the current balance. + pub fn can_withdraw_balance_low<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + if T::minimum_balance() == T::Balance::zero() { + return + } + + let account = AccountId::from(10); + let other_account = AccountId::from(100); + let initial_balance = T::minimum_balance() + 5.into(); + T::mint_into(&account, initial_balance).unwrap(); + T::mint_into(&other_account, initial_balance * 2.into()).unwrap(); + + // Verify: can_withdraw below the account balance returns BalanceLow + let ret = T::can_withdraw(&account, initial_balance + 1.into()); + assert_eq!(ret, WithdrawConsequence::BalanceLow); + } + + /// Test [`Inspect::reducible_balance`] returns the full account balance when called with + /// [`Preservation::Expendable`]. + pub fn reducible_balance_expendable<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let account = AccountId::from(10); + let initial_balance = T::minimum_balance() + 10.into(); + T::mint_into(&account, initial_balance).unwrap(); + + // Verify: reducible_balance returns the full balance + let ret = T::reducible_balance(&account, Preservation::Expendable, Fortitude::Polite); + assert_eq!(ret, initial_balance); + } + + /// Tests [`Inspect::reducible_balance`] returns [`Inspect::balance`] - + /// [`Inspect::minimum_balance`] when called with either [`Preservation::Protect`] or + /// [`Preservation::Preserve`]. + pub fn reducible_balance_protect_preserve<T, AccountId>() + where + T: Mutate<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let account = AccountId::from(10); + let initial_balance = T::minimum_balance() + 10.into(); + T::mint_into(&account, initial_balance).unwrap(); + + // Verify: reducible_balance returns the full balance - min balance + let ret = T::reducible_balance(&account, Preservation::Protect, Fortitude::Polite); + assert_eq!(ret, initial_balance - T::minimum_balance()); + let ret = T::reducible_balance(&account, Preservation::Preserve, Fortitude::Polite); + assert_eq!(ret, initial_balance - T::minimum_balance()); + } +} + +pub mod unbalanced { + use crate::traits::{ + fungible::{Inspect, Unbalanced}, + tokens::{Fortitude, Precision, Preservation}, + }; + use core::fmt::Debug; + use sp_arithmetic::{traits::AtLeast8BitUnsigned, ArithmeticError}; + use sp_runtime::{traits::Bounded, TokenError}; + + /// Tests [`Unbalanced::write_balance`]. + /// + /// We don't need to test the Error case for this function, because the trait makes no + /// assumptions about the ways it can fail. That is completely an implementation detail. + pub fn write_balance<T, AccountId>() + where + T: Unbalanced<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + // Setup some accounts to test varying initial balances + let account_0_ed = AccountId::from(0); + let account_1_gt_ed = AccountId::from(1); + let account_2_empty = AccountId::from(2); + T::increase_balance(&account_0_ed, T::minimum_balance(), Precision::Exact).unwrap(); + T::increase_balance(&account_1_gt_ed, T::minimum_balance() + 5.into(), Precision::Exact) + .unwrap(); + + // Test setting the balances of each account by gt the minimum balance succeeds with no + // dust. + let amount = T::minimum_balance() + 10.into(); + assert_eq!(T::write_balance(&account_0_ed, amount), Ok(None)); + assert_eq!(T::write_balance(&account_1_gt_ed, amount), Ok(None)); + assert_eq!(T::write_balance(&account_2_empty, amount), Ok(None)); + assert_eq!(T::balance(&account_0_ed), amount); + assert_eq!(T::balance(&account_1_gt_ed), amount); + assert_eq!(T::balance(&account_2_empty), amount); + + // Test setting the balances of each account to below the minimum balance succeeds with + // the expected dust. + // If the minimum balance is 1, then the dust is 0, represented as None. + // If the minimum balance is >1, then the dust is the remaining balance that will be wiped + // as the account is reaped. + let amount = T::minimum_balance() - 1.into(); + if T::minimum_balance() == 1.into() { + assert_eq!(T::write_balance(&account_0_ed, amount), Ok(None)); + assert_eq!(T::write_balance(&account_1_gt_ed, amount), Ok(None)); + assert_eq!(T::write_balance(&account_2_empty, amount), Ok(None)); + } else if T::minimum_balance() > 1.into() { + assert_eq!(T::write_balance(&account_0_ed, amount), Ok(Some(amount))); + assert_eq!(T::write_balance(&account_1_gt_ed, amount), Ok(Some(amount))); + assert_eq!(T::write_balance(&account_2_empty, amount), Ok(Some(amount))); + } + } + + /// Tests [`Unbalanced::decrease_balance`] called with [`Preservation::Expendable`]. + pub fn decrease_balance_expendable<T, AccountId>() + where + T: Unbalanced<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + // Setup account with some balance + let account_0 = AccountId::from(0); + let account_0_initial_balance = T::minimum_balance() + 10.into(); + T::increase_balance(&account_0, account_0_initial_balance, Precision::Exact).unwrap(); + + // Decreasing the balance still above the minimum balance should not reap the account. + let amount = 1.into(); + assert_eq!( + T::decrease_balance( + &account_0, + amount, + Precision::Exact, + Preservation::Expendable, + Fortitude::Polite, + ), + Ok(amount), + ); + assert_eq!(T::balance(&account_0), account_0_initial_balance - amount); + + // Decreasing the balance below funds avalibale should fail when Precision::Exact + let balance_before = T::balance(&account_0); + assert_eq!( + T::decrease_balance( + &account_0, + account_0_initial_balance, + Precision::Exact, + Preservation::Expendable, + Fortitude::Polite, + ), + Err(TokenError::FundsUnavailable.into()) + ); + // Balance unchanged + assert_eq!(T::balance(&account_0), balance_before); + + // And reap the account when Precision::BestEffort + assert_eq!( + T::decrease_balance( + &account_0, + account_0_initial_balance, + Precision::BestEffort, + Preservation::Expendable, + Fortitude::Polite, + ), + Ok(balance_before), + ); + // Account reaped + assert_eq!(T::balance(&account_0), 0.into()); + } + + /// Tests [`Unbalanced::decrease_balance`] called with [`Preservation::Preserve`]. + pub fn decrease_balance_preserve<T, AccountId>() + where + T: Unbalanced<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + // Setup account with some balance + let account_0 = AccountId::from(0); + let account_0_initial_balance = T::minimum_balance() + 10.into(); + T::increase_balance(&account_0, account_0_initial_balance, Precision::Exact).unwrap(); + + // Decreasing the balance below the minimum when Precision::Exact should fail. + let amount = 11.into(); + assert_eq!( + T::decrease_balance( + &account_0, + amount, + Precision::Exact, + Preservation::Preserve, + Fortitude::Polite, + ), + Err(TokenError::BelowMinimum.into()), + ); + // Balance should not have changed. + assert_eq!(T::balance(&account_0), account_0_initial_balance); + + // Decreasing the balance below the minimum when Precision::BestEffort should reduce to + // minimum balance. + let amount = 11.into(); + assert_eq!( + T::decrease_balance( + &account_0, + amount, + Precision::BestEffort, + Preservation::Preserve, + Fortitude::Polite, + ), + Ok(account_0_initial_balance - T::minimum_balance()), + ); + assert_eq!(T::balance(&account_0), T::minimum_balance()); + } + + /// Tests [`Unbalanced::increase_balance`]. + pub fn increase_balance<T, AccountId>() + where + T: Unbalanced<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let account_0 = AccountId::from(0); + assert_eq!(T::balance(&account_0), 0.into()); + + // Increasing the bal below the ED errors when precision is Exact + if T::minimum_balance() > 0.into() { + assert_eq!( + T::increase_balance(&account_0, T::minimum_balance() - 1.into(), Precision::Exact), + Err(TokenError::BelowMinimum.into()), + ); + } + assert_eq!(T::balance(&account_0), 0.into()); + + // Increasing the bal below the ED leaves the balance at zero when precision is BestEffort + if T::minimum_balance() > 0.into() { + assert_eq!( + T::increase_balance( + &account_0, + T::minimum_balance() - 1.into(), + Precision::BestEffort + ), + Ok(0.into()), + ); + } + assert_eq!(T::balance(&account_0), 0.into()); + + // Can increase if new bal is >= ED + assert_eq!( + T::increase_balance(&account_0, T::minimum_balance(), Precision::Exact), + Ok(T::minimum_balance()), + ); + assert_eq!(T::balance(&account_0), T::minimum_balance()); + assert_eq!(T::increase_balance(&account_0, 5.into(), Precision::Exact), Ok(5.into()),); + assert_eq!(T::balance(&account_0), T::minimum_balance() + 5.into()); + + // Increasing by amount that would overflow fails when precision is Exact + assert_eq!( + T::increase_balance(&account_0, T::Balance::max_value(), Precision::Exact), + Err(ArithmeticError::Overflow.into()), + ); + + // Increasing by amount that would overflow saturates when precision is BestEffort + let balance_before = T::balance(&account_0); + assert_eq!( + T::increase_balance(&account_0, T::Balance::max_value(), Precision::BestEffort), + Ok(T::Balance::max_value() - balance_before), + ); + assert_eq!(T::balance(&account_0), T::Balance::max_value()); + } + + /// Tests [`Unbalanced::set_total_issuance`]. + pub fn set_total_issuance<T, AccountId>() + where + T: Unbalanced<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + T::set_total_issuance(1.into()); + assert_eq!(T::total_issuance(), 1.into()); + + T::set_total_issuance(0.into()); + assert_eq!(T::total_issuance(), 0.into()); + + T::set_total_issuance(T::minimum_balance()); + assert_eq!(T::total_issuance(), T::minimum_balance()); + + T::set_total_issuance(T::minimum_balance() + 5.into()); + assert_eq!(T::total_issuance(), T::minimum_balance() + 5.into()); + + if T::minimum_balance() > 0.into() { + T::set_total_issuance(T::minimum_balance() - 1.into()); + assert_eq!(T::total_issuance(), T::minimum_balance() - 1.into()); + } + } + + /// Tests [`Unbalanced::deactivate`] and [`Unbalanced::reactivate`]. + pub fn deactivate_and_reactivate<T, AccountId>() + where + T: Unbalanced<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + T::set_total_issuance(10.into()); + assert_eq!(T::total_issuance(), 10.into()); + assert_eq!(T::active_issuance(), 10.into()); + + T::deactivate(2.into()); + assert_eq!(T::total_issuance(), 10.into()); + assert_eq!(T::active_issuance(), 8.into()); + + // Saturates at total_issuance + T::reactivate(4.into()); + assert_eq!(T::total_issuance(), 10.into()); + assert_eq!(T::active_issuance(), 10.into()); + + // Decrements correctly after saturating at total_issuance + T::deactivate(1.into()); + assert_eq!(T::total_issuance(), 10.into()); + assert_eq!(T::active_issuance(), 9.into()); + + // Saturates at zero + T::deactivate(15.into()); + assert_eq!(T::total_issuance(), 10.into()); + assert_eq!(T::active_issuance(), 0.into()); + + // Increments correctly after saturating at zero + T::reactivate(1.into()); + assert_eq!(T::total_issuance(), 10.into()); + assert_eq!(T::active_issuance(), 1.into()); + } +} + +pub mod balanced { + use crate::traits::{ + fungible::{Balanced, Inspect}, + tokens::{imbalance::Imbalance as ImbalanceT, Fortitude, Precision, Preservation}, + }; + use core::fmt::Debug; + use frame_support::traits::tokens::fungible::imbalance::{Credit, Debt}; + use sp_arithmetic::{traits::AtLeast8BitUnsigned, ArithmeticError}; + use sp_runtime::{traits::Bounded, TokenError}; + + /// Tests issuing and resolving [`Credit`] imbalances with [`Balanced::issue`] and + /// [`Balanced::resolve`]. + pub fn issue_and_resolve_credit<T, AccountId>() + where + T: Balanced<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let account = AccountId::from(0); + assert_eq!(T::total_issuance(), 0.into()); + assert_eq!(T::balance(&account), 0.into()); + + // Account that doesn't exist yet can't be credited below the minimum balance + let credit: Credit<AccountId, T> = T::issue(T::minimum_balance() - 1.into()); + // issue temporarily increases total issuance + assert_eq!(T::total_issuance(), credit.peek()); + match T::resolve(&account, credit) { + Ok(_) => panic!("Balanced::resolve should have failed"), + Err(c) => assert_eq!(c.peek(), T::minimum_balance() - 1.into()), + }; + // Credit was unused and dropped from total issuance + assert_eq!(T::total_issuance(), 0.into()); + assert_eq!(T::balance(&account), 0.into()); + + // Credit account with minimum balance + let credit: Credit<AccountId, T> = T::issue(T::minimum_balance()); + match T::resolve(&account, credit) { + Ok(()) => {}, + Err(_) => panic!("resolve failed"), + }; + assert_eq!(T::total_issuance(), T::minimum_balance()); + assert_eq!(T::balance(&account), T::minimum_balance()); + + // Now that account has been created, it can be credited with an amount below the minimum + // balance. + let total_issuance_before = T::total_issuance(); + let balance_before = T::balance(&account); + let amount = T::minimum_balance() - 1.into(); + let credit: Credit<AccountId, T> = T::issue(amount); + match T::resolve(&account, credit) { + Ok(()) => {}, + Err(_) => panic!("resolve failed"), + }; + assert_eq!(T::total_issuance(), total_issuance_before + amount); + assert_eq!(T::balance(&account), balance_before + amount); + + // Unhandled issuance is dropped from total issuance + // `let _ = ...` immediately drops the issuance, so everything should be unchanged when + // logic gets to the assertions. + let total_issuance_before = T::total_issuance(); + let balance_before = T::balance(&account); + let _ = T::issue(5.into()); + assert_eq!(T::total_issuance(), total_issuance_before); + assert_eq!(T::balance(&account), balance_before); + } + + /// Tests issuing and resolving [`Debt`] imbalances with [`Balanced::rescind`] and + /// [`Balanced::settle`]. + pub fn rescind_and_settle_debt<T, AccountId>() + where + T: Balanced<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + // Credit account with some balance + let account = AccountId::from(0); + let initial_bal = T::minimum_balance() + 10.into(); + let credit = T::issue(initial_bal); + match T::resolve(&account, credit) { + Ok(()) => {}, + Err(_) => panic!("resolve failed"), + }; + assert_eq!(T::total_issuance(), initial_bal); + assert_eq!(T::balance(&account), initial_bal); + + // Rescind some balance + let rescind_amount = 2.into(); + let debt: Debt<AccountId, T> = T::rescind(rescind_amount); + assert_eq!(debt.peek(), rescind_amount); + match T::settle(&account, debt, Preservation::Expendable) { + Ok(c) => { + // We settled the full debt and account was not dusted, so there is no left over + // credit. + assert_eq!(c.peek(), 0.into()); + }, + Err(_) => panic!("settle failed"), + }; + assert_eq!(T::total_issuance(), initial_bal - rescind_amount); + assert_eq!(T::balance(&account), initial_bal - rescind_amount); + + // Unhandled debt is added from total issuance + // `let _ = ...` immediately drops the debt, so everything should be unchanged when + // logic gets to the assertions. + let _ = T::rescind(T::minimum_balance()); + assert_eq!(T::total_issuance(), initial_bal - rescind_amount); + assert_eq!(T::balance(&account), initial_bal - rescind_amount); + + // Preservation::Preserve will not allow the account to be dusted on settle + let balance_before = T::balance(&account); + let total_issuance_before = T::total_issuance(); + let rescind_amount = balance_before - T::minimum_balance() + 1.into(); + let debt: Debt<AccountId, T> = T::rescind(rescind_amount); + assert_eq!(debt.peek(), rescind_amount); + // The new debt is temporarily removed from total_issuance + assert_eq!(T::total_issuance(), total_issuance_before - debt.peek().into()); + match T::settle(&account, debt, Preservation::Preserve) { + Ok(_) => panic!("Balanced::settle should have failed"), + Err(d) => assert_eq!(d.peek(), rescind_amount), + }; + // The debt is added back to total_issuance because it was dropped, leaving the operation a + // noop. + assert_eq!(T::total_issuance(), total_issuance_before); + assert_eq!(T::balance(&account), balance_before); + + // Preservation::Expendable allows the account to be dusted on settle + let debt: Debt<AccountId, T> = T::rescind(rescind_amount); + match T::settle(&account, debt, Preservation::Expendable) { + Ok(c) => { + // Dusting happens internally, there is no left over credit. + assert_eq!(c.peek(), 0.into()); + }, + Err(_) => panic!("settle failed"), + }; + // The account is dusted and debt dropped from total_issuance + assert_eq!(T::total_issuance(), 0.into()); + assert_eq!(T::balance(&account), 0.into()); + } + + /// Tests [`Balanced::deposit`]. + pub fn deposit<T, AccountId>() + where + T: Balanced<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + // Cannot deposit < minimum balance into non-existent account + let account = AccountId::from(0); + let amount = T::minimum_balance() - 1.into(); + match T::deposit(&account, amount, Precision::Exact) { + Ok(_) => panic!("Balanced::deposit should have failed"), + Err(e) => assert_eq!(e, TokenError::BelowMinimum.into()), + }; + assert_eq!(T::total_issuance(), 0.into()); + assert_eq!(T::balance(&account), 0.into()); + + // Can deposit minimum balance into non-existent account + let amount = T::minimum_balance(); + match T::deposit(&account, amount, Precision::Exact) { + Ok(d) => assert_eq!(d.peek(), amount), + Err(_) => panic!("Balanced::deposit failed"), + }; + assert_eq!(T::total_issuance(), amount); + assert_eq!(T::balance(&account), amount); + + // Depositing amount that would overflow when Precision::Exact fails and is a noop + let amount = T::Balance::max_value(); + let balance_before = T::balance(&account); + let total_issuance_before = T::total_issuance(); + match T::deposit(&account, amount, Precision::Exact) { + Ok(_) => panic!("Balanced::deposit should have failed"), + Err(e) => assert_eq!(e, ArithmeticError::Overflow.into()), + }; + assert_eq!(T::total_issuance(), total_issuance_before); + assert_eq!(T::balance(&account), balance_before); + + // Depositing amount that would overflow when Precision::BestEffort saturates + match T::deposit(&account, amount, Precision::BestEffort) { + Ok(d) => assert_eq!(d.peek(), T::Balance::max_value() - balance_before), + Err(_) => panic!("Balanced::deposit failed"), + }; + assert_eq!(T::total_issuance(), T::Balance::max_value()); + assert_eq!(T::balance(&account), T::Balance::max_value()); + } + + /// Tests [`Balanced::withdraw`]. + pub fn withdraw<T, AccountId>() + where + T: Balanced<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + let account = AccountId::from(0); + + // Init an account with some balance + let initial_balance = T::minimum_balance() + 10.into(); + match T::deposit(&account, initial_balance, Precision::Exact) { + Ok(_) => {}, + Err(_) => panic!("Balanced::deposit failed"), + }; + assert_eq!(T::total_issuance(), initial_balance); + assert_eq!(T::balance(&account), initial_balance); + + // Withdrawing an amount smaller than the balance works when Precision::Exact + let amount = 1.into(); + match T::withdraw( + &account, + amount, + Precision::Exact, + Preservation::Expendable, + Fortitude::Polite, + ) { + Ok(c) => assert_eq!(c.peek(), amount), + Err(_) => panic!("withdraw failed"), + }; + assert_eq!(T::total_issuance(), initial_balance - amount); + assert_eq!(T::balance(&account), initial_balance - amount); + + // Withdrawing an amount greater than the balance fails when Precision::Exact + let balance_before = T::balance(&account); + let amount = balance_before + 1.into(); + match T::withdraw( + &account, + amount, + Precision::Exact, + Preservation::Expendable, + Fortitude::Polite, + ) { + Ok(_) => panic!("should have failed"), + Err(e) => assert_eq!(e, TokenError::FundsUnavailable.into()), + }; + assert_eq!(T::total_issuance(), balance_before); + assert_eq!(T::balance(&account), balance_before); + + // Withdrawing an amount greater than the balance works when Precision::BestEffort + let balance_before = T::balance(&account); + let amount = balance_before + 1.into(); + match T::withdraw( + &account, + amount, + Precision::BestEffort, + Preservation::Expendable, + Fortitude::Polite, + ) { + Ok(c) => assert_eq!(c.peek(), balance_before), + Err(_) => panic!("withdraw failed"), + }; + assert_eq!(T::total_issuance(), 0.into()); + assert_eq!(T::balance(&account), 0.into()); + } + + /// Tests [`Balanced::pair`]. + pub fn pair<T, AccountId>() + where + T: Balanced<AccountId>, + <T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, + { + // Pair zero balance works + let (credit, debt) = T::pair(0.into()); + assert_eq!(debt.peek(), 0.into()); + assert_eq!(credit.peek(), 0.into()); + + // Pair with non-zero balance: the credit and debt cancel each other out + let balance = 10.into(); + let (credit, debt) = T::pair(balance); + assert_eq!(credit.peek(), balance); + assert_eq!(debt.peek(), balance); + + // Pair with max balance: the credit and debt still cancel each other out + let balance = T::Balance::max_value() - 1.into(); + let (debt, credit) = T::pair(balance); + assert_eq!(debt.peek(), balance); + assert_eq!(credit.peek(), balance); + } +} diff --git a/substrate/frame/support/src/traits/tokens/fungible/item_of.rs b/substrate/frame/support/src/traits/tokens/fungible/item_of.rs index cf2d96ef287..b0b8bbcf7be 100644 --- a/substrate/frame/support/src/traits/tokens/fungible/item_of.rs +++ b/substrate/frame/support/src/traits/tokens/fungible/item_of.rs @@ -386,7 +386,7 @@ impl< fn issue(amount: Self::Balance) -> Credit<AccountId, Self> { Imbalance::new(<F as fungibles::Balanced<AccountId>>::issue(A::get(), amount).peek()) } - fn pair(amount: Self::Balance) -> (Debt<AccountId, Self>, Credit<AccountId, Self>) { + fn pair(amount: Self::Balance) -> (Credit<AccountId, Self>, Debt<AccountId, Self>) { let (a, b) = <F as fungibles::Balanced<AccountId>>::pair(A::get(), amount); (Imbalance::new(a.peek()), Imbalance::new(b.peek())) } diff --git a/substrate/frame/support/src/traits/tokens/fungible/regular.rs b/substrate/frame/support/src/traits/tokens/fungible/regular.rs index 2838bed540a..1b3e971de46 100644 --- a/substrate/frame/support/src/traits/tokens/fungible/regular.rs +++ b/substrate/frame/support/src/traits/tokens/fungible/regular.rs @@ -65,7 +65,7 @@ pub trait Inspect<AccountId>: Sized { /// indefinitely. /// /// For the amount of the balance which is currently free to be removed from the account without - /// error, use `reducible_balance`. + /// error, use [`Inspect::reducible_balance`]. /// /// For the amount of the balance which may eventually be free to be removed from the account, /// use `balance()`. @@ -75,7 +75,7 @@ pub trait Inspect<AccountId>: Sized { /// subsystems of the chain ("on hold" or "reserved"). /// /// In general this isn't especially useful outside of tests, and for practical purposes, you'll - /// want to use `reducible_balance()`. + /// want to use [`Inspect::reducible_balance`]. fn balance(who: &AccountId) -> Self::Balance; /// Get the maximum amount that `who` can withdraw/transfer successfully based on whether the @@ -83,7 +83,7 @@ pub trait Inspect<AccountId>: Sized { /// reduction and potentially go below user-level restrictions on the minimum amount of the /// account. /// - /// Always less than or equal to `balance()`. + /// Always less than or equal to [`Inspect::balance`]. fn reducible_balance( who: &AccountId, preservation: Preservation, @@ -107,7 +107,7 @@ pub trait Inspect<AccountId>: Sized { fn can_withdraw(who: &AccountId, amount: Self::Balance) -> WithdrawConsequence<Self::Balance>; } -/// Special dust type which can be type-safely converted into a `Credit`. +/// Special dust type which can be type-safely converted into a [`Credit`]. #[must_use] pub struct Dust<A, T: Inspect<A>>(pub T::Balance); @@ -124,20 +124,20 @@ impl<A, T: Balanced<A>> Dust<A, T> { /// Do not use this directly unless you want trouble, since it allows you to alter account balances /// without keeping the issuance up to date. It has no safeguards against accidentally creating /// token imbalances in your system leading to accidental inflation or deflation. It's really just -/// for the underlying datatype to implement so the user gets the much safer `Balanced` trait to +/// for the underlying datatype to implement so the user gets the much safer [`Balanced`] trait to /// use. pub trait Unbalanced<AccountId>: Inspect<AccountId> { - /// Create some dust and handle it with `Self::handle_dust`. This is an unbalanced operation - /// and it must only be used when an account is modified in a raw fashion, outside of the entire - /// fungibles API. The `amount` is capped at `Self::minimum_balance() - 1`. + /// Create some dust and handle it with [`Unbalanced::handle_dust`]. This is an unbalanced + /// operation and it must only be used when an account is modified in a raw fashion, outside of + /// the entire fungibles API. The `amount` is capped at [`Inspect::minimum_balance()`] - 1`. /// /// This should not be reimplemented. fn handle_raw_dust(amount: Self::Balance) { Self::handle_dust(Dust(amount.min(Self::minimum_balance().saturating_sub(One::one())))) } - /// Do something with the dust which has been destroyed from the system. `Dust` can be converted - /// into a `Credit` with the `Balanced` trait impl. + /// Do something with the dust which has been destroyed from the system. [`Dust`] can be + /// converted into a [`Credit`] with the [`Balanced`] trait impl. fn handle_dust(dust: Dust<AccountId, Self>); /// Forcefully set the balance of `who` to `amount`. @@ -152,9 +152,10 @@ pub trait Unbalanced<AccountId>: Inspect<AccountId> { /// If this cannot be done for some reason (e.g. because the account cannot be created, deleted /// or would overflow) then an `Err` is returned. /// - /// If `Ok` is returned then its inner, if `Some` is the amount which was discarded as dust due - /// to existential deposit requirements. The default implementation of `decrease_balance` and - /// `increase_balance` converts this into an `Imbalance` and then passes it into `handle_dust`. + /// If `Ok` is returned then its inner, then `Some` is the amount which was discarded as dust + /// due to existential deposit requirements. The default implementation of + /// [`Unbalanced::decrease_balance`] and [`Unbalanced::increase_balance`] converts this into an + /// [`Imbalance`] and then passes it into [`Unbalanced::handle_dust`]. fn write_balance( who: &AccountId, amount: Self::Balance, @@ -165,14 +166,14 @@ pub trait Unbalanced<AccountId>: Inspect<AccountId> { /// Reduce the balance of `who` by `amount`. /// - /// If `precision` is `Exact` and it cannot be reduced by that amount for - /// some reason, return `Err` and don't reduce it at all. If `precision` is `BestEffort`, then + /// If `precision` is [`Exact`] and it cannot be reduced by that amount for + /// some reason, return `Err` and don't reduce it at all. If `precision` is [`BestEffort`], then /// reduce the balance of `who` by the most that is possible, up to `amount`. /// /// In either case, if `Ok` is returned then the inner is the amount by which is was reduced. /// Minimum balance will be respected and thus the returned amount may be up to - /// `Self::minimum_balance() - 1` greater than `amount` in the case that the reduction caused - /// the account to be deleted. + /// [`Inspect::minimum_balance()`] - 1` greater than `amount` in the case that the reduction + /// caused the account to be deleted. fn decrease_balance( who: &AccountId, mut amount: Self::Balance, @@ -182,9 +183,14 @@ pub trait Unbalanced<AccountId>: Inspect<AccountId> { ) -> Result<Self::Balance, DispatchError> { let old_balance = Self::balance(who); let free = Self::reducible_balance(who, preservation, force); - if let BestEffort = precision { + if precision == BestEffort { amount = amount.min(free); } + + // Under no circumsances should the account go below free when preservation is Preserve. + if amount > free && preservation == Preservation::Preserve { + return Err(TokenError::BelowMinimum.into()) + } let new_balance = old_balance.checked_sub(&amount).ok_or(TokenError::FundsUnavailable)?; if let Some(dust) = Self::write_balance(who, new_balance)? { Self::handle_dust(Dust(dust)); @@ -197,7 +203,7 @@ pub trait Unbalanced<AccountId>: Inspect<AccountId> { /// If it cannot be increased by that amount for some reason, return `Err` and don't increase /// it at all. If Ok, return the imbalance. /// Minimum balance will be respected and an error will be returned if - /// `amount < Self::minimum_balance()` when the account of `who` is zero. + /// amount < [`Inspect::minimum_balance()`] when the account of `who` is zero. fn increase_balance( who: &AccountId, amount: Self::Balance, @@ -267,8 +273,8 @@ pub trait Mutate<AccountId>: Inspect<AccountId> + Unbalanced<AccountId> { /// Attempt to decrease the `asset` balance of `who` by `amount`. /// - /// Equivalent to `burn_from`, except with an expectation that within the bounds of some - /// universal issuance, the total assets `suspend`ed and `resume`d will be equivalent. The + /// Equivalent to [`Mutate::burn_from`], except with an expectation that within the bounds of + /// some universal issuance, the total assets `suspend`ed and `resume`d will be equivalent. The /// implementation may be configured such that the total assets suspended may never be less than /// the total assets resumed (which is the invariant for an issuing system), or the reverse /// (which the invariant in a non-issuing system). @@ -287,8 +293,8 @@ pub trait Mutate<AccountId>: Inspect<AccountId> + Unbalanced<AccountId> { /// Attempt to increase the `asset` balance of `who` by `amount`. /// - /// Equivalent to `mint_into`, except with an expectation that within the bounds of some - /// universal issuance, the total assets `suspend`ed and `resume`d will be equivalent. The + /// Equivalent to [`Mutate::mint_into`], except with an expectation that within the bounds of + /// some universal issuance, the total assets `suspend`ed and `resume`d will be equivalent. The /// implementation may be configured such that the total assets suspended may never be less than /// the total assets resumed (which is the invariant for an issuing system), or the reverse /// (which the invariant in a non-issuing system). @@ -367,7 +373,7 @@ impl<AccountId, U: Unbalanced<AccountId>> HandleImbalanceDrop<U::Balance> /// A fungible token class where any creation and deletion of tokens is semi-explicit and where the /// total supply is maintained automatically. /// -/// This is auto-implemented when a token class has `Unbalanced` implemented. +/// This is auto-implemented when a token class has [`Unbalanced`] implemented. pub trait Balanced<AccountId>: Inspect<AccountId> + Unbalanced<AccountId> { /// The type for managing what happens when an instance of `Debt` is dropped without being used. type OnDropDebt: HandleImbalanceDrop<Self::Balance>; @@ -376,7 +382,7 @@ pub trait Balanced<AccountId>: Inspect<AccountId> + Unbalanced<AccountId> { type OnDropCredit: HandleImbalanceDrop<Self::Balance>; /// Reduce the total issuance by `amount` and return the according imbalance. The imbalance will - /// typically be used to reduce an account by the same amount with e.g. `settle`. + /// typically be used to reduce an account by the same amount with e.g. [`Balanced::settle`]. /// /// This is infallible, but doesn't guarantee that the entire `amount` is burnt, for example /// in the case of underflow. @@ -391,7 +397,7 @@ pub trait Balanced<AccountId>: Inspect<AccountId> + Unbalanced<AccountId> { /// Increase the total issuance by `amount` and return the according imbalance. The imbalance /// will typically be used to increase an account by the same amount with e.g. - /// `resolve_into_existing` or `resolve_creating`. + /// [`Balanced::resolve`]. /// /// This is infallible, but doesn't guarantee that the entire `amount` is issued, for example /// in the case of overflow. @@ -408,18 +414,18 @@ pub trait Balanced<AccountId>: Inspect<AccountId> + Unbalanced<AccountId> { /// /// This is just the same as burning and issuing the same amount and has no effect on the /// total issuance. - fn pair(amount: Self::Balance) -> (Debt<AccountId, Self>, Credit<AccountId, Self>) { - (Self::rescind(amount), Self::issue(amount)) + fn pair(amount: Self::Balance) -> (Credit<AccountId, Self>, Debt<AccountId, Self>) { + (Self::issue(amount), Self::rescind(amount)) } /// Mints `value` into the account of `who`, creating it as needed. /// /// If `precision` is `BestEffort` and `value` in full could not be minted (e.g. due to - /// overflow), then the maximum is minted, up to `value`. If `precision` is `Exact`, then + /// overflow), then the maximum is minted, up to `value`. If `precision` is [`Exact`], then /// exactly `value` must be minted into the account of `who` or the operation will fail with an /// `Err` and nothing will change. /// - /// If the operation is successful, this will return `Ok` with a `Debt` of the total value + /// If the operation is successful, this will return `Ok` with a [`Debt`] of the total value /// added to the account. fn deposit( who: &AccountId, @@ -433,8 +439,8 @@ pub trait Balanced<AccountId>: Inspect<AccountId> + Unbalanced<AccountId> { /// Removes `value` balance from `who` account if possible. /// - /// If `precision` is `BestEffort` and `value` in full could not be removed (e.g. due to - /// underflow), then the maximum is removed, up to `value`. If `precision` is `Exact`, then + /// If `precision` is [`BestEffort`] and `value` in full could not be removed (e.g. due to + /// underflow), then the maximum is removed, up to `value`. If `precision` is [`Exact`], then /// exactly `value` must be removed from the account of `who` or the operation will fail with an /// `Err` and nothing will change. /// @@ -442,7 +448,7 @@ pub trait Balanced<AccountId>: Inspect<AccountId> + Unbalanced<AccountId> { /// If the account needed to be deleted, then slightly more than `value` may be removed from the /// account owning since up to (but not including) minimum balance may also need to be removed. /// - /// If the operation is successful, this will return `Ok` with a `Credit` of the total value + /// If the operation is successful, this will return `Ok` with a [`Credit`] of the total value /// removed from the account. fn withdraw( who: &AccountId, @@ -460,7 +466,7 @@ pub trait Balanced<AccountId>: Inspect<AccountId> + Unbalanced<AccountId> { /// cannot be countered, then nothing is changed and the original `credit` is returned in an /// `Err`. /// - /// Please note: If `credit.peek()` is less than `Self::minimum_balance()`, then `who` must + /// Please note: If `credit.peek()` is less than [`Inspect::minimum_balance()`], then `who` must /// already exist for this to succeed. fn resolve( who: &AccountId, @@ -487,7 +493,7 @@ pub trait Balanced<AccountId>: Inspect<AccountId> + Unbalanced<AccountId> { let amount = debt.peek(); let credit = match Self::withdraw(who, amount, Exact, preservation, Polite) { Err(_) => return Err(debt), - Ok(d) => d, + Ok(c) => c, }; match credit.offset(debt) { -- GitLab