From ccbe9aaaf92286497316eb8e130fd22d831eca62 Mon Sep 17 00:00:00 2001 From: Liam Aharon Date: Fri, 5 May 2023 02:21:40 +1000 Subject: [PATCH] fungible conformance tests: Inspect and Mutate (#13852) * typo * - create test files for each fungile trait - begin implementing tests for the Inspect trait * wrap inspect tests in a macro * first run of mutate tests * move test implementation out of ballances * make tests more generic * transfer tests * combine inspect and mutate tests * set balance failing tests * can_deposit tests * can_withdraw tests * test reducible_balance * remove balanced stub * revert set_balance return val fix * typo * macro and dust trap tests * disable test when it doesn't make sense * remove debug comment * reduce macro boilerplate * improved var naming * improve variable naming * remove redundant comment * remove placeholder tests * remove placeholder tests * simplify macro * Update frame/balances/src/tests/fungible_conformance_tests.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * use Balance from T * fix copyright * add test doc comments * improve test naming * clippy * fix rustdoc errors * fix rustdoc * improve macro * improve variable naming * remove redundant comment * use path --------- Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> --- Cargo.lock | 1 + frame/balances/Cargo.toml | 1 + .../src/tests/fungible_conformance_tests.rs | 88 ++ frame/balances/src/tests/mod.rs | 1 + .../conformance_tests/inspect_mutate.rs | 975 ++++++++++++++++++ .../tokens/fungible/conformance_tests/mod.rs | 1 + .../support/src/traits/tokens/fungible/mod.rs | 1 + .../src/traits/tokens/fungible/regular.rs | 2 +- 8 files changed, 1069 insertions(+), 1 deletion(-) create mode 100644 frame/balances/src/tests/fungible_conformance_tests.rs create mode 100644 frame/support/src/traits/tokens/fungible/conformance_tests/inspect_mutate.rs create mode 100644 frame/support/src/traits/tokens/fungible/conformance_tests/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 3f1fd0bd5802e..41e059366266d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5867,6 +5867,7 @@ dependencies = [ "log", "pallet-transaction-payment", "parity-scale-codec", + "paste", "scale-info", "sp-core", "sp-io", diff --git a/frame/balances/Cargo.toml b/frame/balances/Cargo.toml index b3457dd8d141c..53ea04fc12ea3 100644 --- a/frame/balances/Cargo.toml +++ b/frame/balances/Cargo.toml @@ -26,6 +26,7 @@ sp-std = { version = "5.0.0", default-features = false, path = "../../primitives pallet-transaction-payment = { version = "4.0.0-dev", path = "../transaction-payment" } sp-core = { version = "7.0.0", path = "../../primitives/core" } sp-io = { version = "7.0.0", path = "../../primitives/io" } +paste = "1.0.12" [features] default = ["std"] diff --git a/frame/balances/src/tests/fungible_conformance_tests.rs b/frame/balances/src/tests/fungible_conformance_tests.rs new file mode 100644 index 0000000000000..6262aa04dc088 --- /dev/null +++ b/frame/balances/src/tests/fungible_conformance_tests.rs @@ -0,0 +1,88 @@ +// 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. + +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),*) => { + $( + paste! { + #[test] + fn [< $name _existential_deposit_ $ext_deposit _dust_trap_on >]() { + let trap_account = ::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::< + Balances, + ::AccountId, + >(Some(trap_account)); + }); + } + + #[test] + fn [< $name _existential_deposit_ $ext_deposit _dust_trap_off >]() { + let builder = ExtBuilder::default().existential_deposit($ext_deposit); + builder.build_and_execute_with(|| { + $path::$name::< + Balances, + ::AccountId, + >(None); + }); + } + } + )* + }; + ($path:path, $ext_deposit:expr) => { + run_tests!( + $path, + $ext_deposit, + mint_into_success, + mint_into_overflow, + mint_into_below_minimum, + burn_from_exact_success, + burn_from_best_effort_success, + burn_from_exact_insufficient_funds, + restore_success, + restore_overflow, + restore_below_minimum, + shelve_success, + shelve_insufficient_funds, + transfer_success, + transfer_expendable_all, + transfer_expendable_dust, + transfer_protect_preserve, + set_balance_mint_success, + set_balance_burn_success, + can_deposit_success, + can_deposit_below_minimum, + can_deposit_overflow, + can_withdraw_success, + can_withdraw_reduced_to_zero, + can_withdraw_balance_low, + reducible_balance_expendable, + reducible_balance_protect_preserve + ); + }; +} + +run_tests!(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); diff --git a/frame/balances/src/tests/mod.rs b/frame/balances/src/tests/mod.rs index 9b451f36d1d94..259051ed46e5b 100644 --- a/frame/balances/src/tests/mod.rs +++ b/frame/balances/src/tests/mod.rs @@ -45,6 +45,7 @@ use sp_runtime::{ mod currency_tests; mod dispatchable_tests; +mod fungible_conformance_tests; mod fungible_tests; mod reentrancy_tests; diff --git a/frame/support/src/traits/tokens/fungible/conformance_tests/inspect_mutate.rs b/frame/support/src/traits/tokens/fungible/conformance_tests/inspect_mutate.rs new file mode 100644 index 0000000000000..732742cca9b54 --- /dev/null +++ b/frame/support/src/traits/tokens/fungible/conformance_tests/inspect_mutate.rs @@ -0,0 +1,975 @@ +// 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. + +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 the `mint_into` function for successful token minting. +/// +/// This test checks the `mint_into` function in the `Mutate` trait implementation for type `T`. +/// It ensures that account balances and total issuance values are updated correctly after minting +/// tokens into two distinct accounts. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn mint_into_success(_dust_trap: Option) +where + T: Mutate, + >::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 the `mint_into` function for overflow prevention. +/// +/// This test ensures that minting tokens beyond the maximum balance value for an account +/// returns an error and does not change the account balance or total issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn mint_into_overflow(_dust_trap: Option) +where + T: Mutate, + >::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 the `mint_into` function for handling balances below the minimum value. +/// +/// This test verifies that minting tokens below the minimum balance for an account +/// returns an error and has no impact on the account balance or total issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn mint_into_below_minimum(_dust_trap: Option) +where + T: Mutate, + >::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 the `burn_from` function for successfully burning an exact amount of tokens. +/// +/// This test checks that the `burn_from` function with `Precision::Exact` correctly +/// reduces the account balance and total issuance values by the burned amount. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate` for `AccountId`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn burn_from_exact_success(_dust_trap: Option) +where + T: Mutate, + >::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 the `burn_from` function for successfully burning tokens with a best-effort approach. +/// +/// This test verifies that the `burn_from` function with `Precision::BestEffort` correctly +/// reduces the account balance and total issuance values by the reducible balance when +/// attempting to burn an amount greater than the reducible balance. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate` for `AccountId`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn burn_from_best_effort_success(_dust_trap: Option) +where + T: Mutate, + >::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 the `burn_from` function for handling insufficient funds with `Precision::Exact`. +/// +/// This test verifies that burning an amount greater than the account's balance with +/// `Precision::Exact` returns an error and does not change the account balance or total issuance +/// values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn burn_from_exact_insufficient_funds(_dust_trap: Option) +where + T: Mutate, + >::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 the `restore` function for successful restoration. +/// +/// This test verifies that restoring an amount into each account updates their balances and the +/// total issuance values correctly. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn restore_success(_dust_trap: Option) +where + T: Mutate, + >::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 the `restore` function for handling balance overflow. +/// +/// This test verifies that restoring an amount beyond the maximum balance returns an error and +/// does not change the account balance or total issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn restore_overflow(_dust_trap: Option) +where + T: Mutate, + >::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 the `restore` function for handling restoration below the minimum balance. +/// +/// This test verifies that restoring an amount below the minimum balance returns an error and +/// does not change the account balance or total issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn restore_below_minimum(_dust_trap: Option) +where + T: Mutate, + >::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 the `shelve` function for successful shelving. +/// +/// This test verifies that shelving an amount from an account reduces the account balance and +/// total issuance values by the shelved amount. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn shelve_success(_dust_trap: Option) +where + T: Mutate, + >::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 the `shelve` function for handling insufficient funds. +/// +/// This test verifies that attempting to shelve an amount greater than the account's balance +/// returns an error and does not change the account balance or total issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn shelve_insufficient_funds(_dust_trap: Option) +where + T: Mutate, + >::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 the `transfer` function for a successful transfer. +/// +/// This test verifies that transferring an amount between two accounts with +/// `Preservation::Expendable` updates the account balances and maintains the total issuance and +/// active issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn transfer_success(_dust_trap: Option) +where + T: Mutate, + >::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 the `transfer` function with `Preservation::Expendable` for transferring the entire +/// balance. +/// +/// This test verifies that transferring the entire balance from one account to another with +/// `Preservation::Expendable` updates the account balances and maintains the total issuance and +/// active issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn transfer_expendable_all(_dust_trap: Option) +where + T: Mutate, + >::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 the transfer function with Preservation::Expendable for transferring amounts that leaves +/// an account with less than the minimum balance. +/// +/// This test verifies that when transferring an amount using Preservation::Expendable and an +/// account will be left with less than the minimum balance, the account balances are updated, dust +/// is collected properly depending on whether a dust_trap exists, and the total issuance and active +/// issuance values remain consistent. +/// +/// # Parameters +/// +/// - dust_trap: An optional account identifier to which dust will be collected. If None, dust will +/// be removed from the total and active issuance. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn transfer_expendable_dust(dust_trap: Option) +where + T: Mutate, + >::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 the `transfer` function with `Preservation::Protect` and `Preservation::Preserve` for +/// transferring the entire balance. +/// +/// This test verifies that attempting to transfer the entire balance with `Preservation::Protect` +/// or `Preservation::Preserve` returns an error, and the account balances, total issuance, and +/// active issuance values remain unchanged. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn transfer_protect_preserve(_dust_trap: Option) +where + T: Mutate, + >::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 the set_balance function for successful minting. +/// +/// This test verifies that minting a balance using set_balance updates the account balance, total +/// issuance, and active issuance correctly. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn set_balance_mint_success(_dust_trap: Option) +where + T: Mutate, + >::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 the set_balance function for successful burning. +/// +/// This test verifies that burning a balance using set_balance updates the account balance, total +/// issuance, and active issuance correctly. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn set_balance_burn_success(_dust_trap: Option) +where + T: Mutate, + >::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 the can_deposit function for returning a success value. +/// +/// This test verifies that the can_deposit function returns DepositConsequence::Success when +/// depositing a reasonable amount. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_deposit_success(_dust_trap: Option) +where + T: Mutate, + >::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 the can_deposit function for returning a minimum balance error. +/// +/// This test verifies that the can_deposit function returns DepositConsequence::BelowMinimum when +/// depositing below the minimum balance. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_deposit_below_minimum(_dust_trap: Option) +where + T: Mutate, + >::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 the can_deposit function for returning an overflow error. +/// +/// This test verifies that the can_deposit function returns DepositConsequence::Overflow when +/// depositing an amount that would cause an overflow. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_deposit_overflow(_dust_trap: Option) +where + T: Mutate, + >::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 the can_withdraw function for returning a success value. +/// +/// This test verifies that the can_withdraw function returns WithdrawConsequence::Success when +/// withdrawing a reasonable amount. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_withdraw_success(_dust_trap: Option) +where + T: Mutate, + >::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 the can_withdraw function for withdrawal resulting in a reduced balance of zero. +/// +/// This test verifies that the can_withdraw function returns WithdrawConsequence::ReducedToZero +/// when withdrawing an amount that would reduce the account balance below the minimum balance. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_withdraw_reduced_to_zero(_dust_trap: Option) +where + T: Mutate, + >::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 the can_withdraw function for returning a low balance error. +/// +/// This test verifies that the can_withdraw function returns WithdrawConsequence::BalanceLow when +/// withdrawing an amount that would result in an account balance below the current balance. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_withdraw_balance_low(_dust_trap: Option) +where + T: Mutate, + >::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 the reducible_balance function with Preservation::Expendable. +/// +/// This test verifies that the reducible_balance function returns the full account balance when +/// using Preservation::Expendable. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn reducible_balance_expendable(_dust_trap: Option) +where + T: Mutate, + >::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); +} + +/// Test the reducible_balance function with Preservation::Protect and Preservation::Preserve. +/// +/// This test verifies that the reducible_balance function returns the account balance minus the +/// minimum balance when using either Preservation::Protect or Preservation::Preserve. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn reducible_balance_protect_preserve(_dust_trap: Option) +where + T: Mutate, + >::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()); +} diff --git a/frame/support/src/traits/tokens/fungible/conformance_tests/mod.rs b/frame/support/src/traits/tokens/fungible/conformance_tests/mod.rs new file mode 100644 index 0000000000000..88ba56a6fed02 --- /dev/null +++ b/frame/support/src/traits/tokens/fungible/conformance_tests/mod.rs @@ -0,0 +1 @@ +pub mod inspect_mutate; diff --git a/frame/support/src/traits/tokens/fungible/mod.rs b/frame/support/src/traits/tokens/fungible/mod.rs index 204b85ac29448..8ab63ad366f08 100644 --- a/frame/support/src/traits/tokens/fungible/mod.rs +++ b/frame/support/src/traits/tokens/fungible/mod.rs @@ -38,6 +38,7 @@ //! funds must be removed from an account before it is known precisely what should be done with //! them. +pub mod conformance_tests; pub mod freeze; pub mod hold; mod imbalance; diff --git a/frame/support/src/traits/tokens/fungible/regular.rs b/frame/support/src/traits/tokens/fungible/regular.rs index 5a201b33b7cd9..3476549464032 100644 --- a/frame/support/src/traits/tokens/fungible/regular.rs +++ b/frame/support/src/traits/tokens/fungible/regular.rs @@ -122,7 +122,7 @@ impl> Dust { /// **WARNING** /// 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 imflation or deflation. It's really just +/// 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 /// use. pub trait Unbalanced: Inspect {