From 6b8788c035fb823185e6f945840a2c80b1d72d86 Mon Sep 17 00:00:00 2001 From: Liam Aharon Date: Mon, 15 Jan 2024 23:30:51 +1100 Subject: [PATCH] Unbalanced and Balanced fungible conformance tests, and fungible fixes (#1296) Original PR https://github.com/paritytech/substrate/pull/14655 --- Partial https://github.com/paritytech/polkadot-sdk/issues/225 - [x] Adds conformance tests for Unbalanced - [x] Adds conformance tests for Balanced - Several minor fixes to fungible default implementations and the Balances pallet - [x] `Unbalanced::decrease_balance` can reap account when `Preservation` is `Preserve` - [x] `Balanced::pair` can return pairs of imbalances which do not cancel each other out - [x] Balances pallet `active_issuance` 'underflow' - [x] Refactors the conformance test file structure to match the fungible file structure: tests for traits in regular.rs go into a test file named regular.rs, tests for traits in freezes.rs go into a test file named freezes.rs, etc. - [x] Improve doc comments - [x] Simplify macros ## Fixes ### `Unbalanced::decrease_balance` can reap account when called with `Preservation::Preserve` There is a potential issue in the default implementation of `Unbalanced::decrease_balance`. The implementation can delete an account even when it is called with `preservation: Preservation::Preserve`. This seems to contradict the documentation of `Preservation::Preserve`: ```rust /// The account may not be killed and our provider reference must remain (in the context of /// tokens, this means that the account may not be dusted). Preserve, ``` I updated `Unbalanced::decrease_balance` to return `Err(TokenError::BelowMinimum)` when a withdrawal would cause the account to be reaped and `preservation: Preservation::Preserve`. - [ ] TODO Confirm with @gavofyork that this is correct behavior Test for this behavior: https://github.com/paritytech/polkadot-sdk/blob/e5c876dd6b59e2b7dbacaa4538cb42c802db3730/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs#L912-L937 ### `Balanced::pair` returning non-canceling pairs `Balanced::pair` is supposed to create a pair of imbalances that cancel each other out. However this is not the case when the method is called with an amount greater than the total supply. In the existing default implementation, `Balanced::pair` creates a pair by first rescinding the balance, creating `Debt`, and then issuing the balance, creating `Credit`. When creating `Debt`, if the amount to create exceeds the `total_supply`, `total_supply` units of `Debt` are created *instead* of `amount` units of `Debt`. This can lead to non-canceling amount of `Credit` and `Debt` being created. To address this, I create the credit and debt directly in the method instead of calling `issue` and `rescind`. Test for this behavior: https://github.com/paritytech/polkadot-sdk/blob/e5c876dd6b59e2b7dbacaa4538cb42c802db3730/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs#L1323-L1346 ### `Balances` pallet `active_issuance` 'underflow' This PR resolves an issue in the `Balances` pallet that can lead to odd behavior of `active_issuance`. Currently, the Balances pallet doesn't check if `InactiveIssuance` remains less than or equal to `TotalIssuance` when supply is deactivated. This allows `InactiveIssuance` to be greater than `TotalIssuance`, which can result in unexpected behavior from the perspective of the fungible API. `active_issuance` is derived from `TotalIssuance.saturating_sub(InactiveIssuance)`. If an `amount` is deactivated that causes `InactiveIssuance` to become greater TotalIssuance, `active_issuance` will return 0. However once in that state, reactivating an amount will not increase `active_issuance` by the reactivated `amount` as expected. Consider this test where the last assertion would fail due to this issue: https://github.com/paritytech/polkadot-sdk/blob/e5c876dd6b59e2b7dbacaa4538cb42c802db3730/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs#L1036-L1071 To address this, I've modified the `deactivate` function to ensure `InactiveIssuance` never surpasses `TotalIssuance`. --------- Co-authored-by: Muharem (cherry picked from commit 46090ff114a131be4139f0abf619330990194770) --- prdoc/pr_1296.prdoc | 16 + substrate/frame/assets/src/tests/sets.rs | 2 +- substrate/frame/balances/src/impl_fungible.rs | 5 +- .../src/tests/fungible_conformance_tests.rs | 81 +- .../tokens/fungible/conformance_tests/mod.rs | 1 + .../conformance_tests/regular/balanced.rs | 292 +++++++ .../fungible/conformance_tests/regular/mod.rs | 20 + .../conformance_tests/regular/mutate.rs | 783 ++++++++++++++++++ .../conformance_tests/regular/unbalanced.rs | 281 +++++++ .../src/traits/tokens/fungible/item_of.rs | 8 +- .../src/traits/tokens/fungible/regular.rs | 97 ++- .../src/traits/tokens/fungible/union_of.rs | 15 +- .../src/traits/tokens/fungibles/regular.rs | 24 +- .../src/traits/tokens/fungibles/union_of.rs | 17 +- 14 files changed, 1563 insertions(+), 79 deletions(-) create mode 100644 prdoc/pr_1296.prdoc create mode 100644 substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular/balanced.rs create mode 100644 substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular/mod.rs create mode 100644 substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular/mutate.rs create mode 100644 substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular/unbalanced.rs diff --git a/prdoc/pr_1296.prdoc b/prdoc/pr_1296.prdoc new file mode 100644 index 000000000000..b7ef4288a57a --- /dev/null +++ b/prdoc/pr_1296.prdoc @@ -0,0 +1,16 @@ +title: fungible fixes and more conformance tests + +doc: + - audience: Runtime Dev + description: | + Adds conformance tests for the Balanced and Unbalanced fungible traits + Fixes Unbalanced::decrease_balance not respecting preservation + Fixes Balanced::pair possibly returning pairs of imbalances which do not cancel each other out. Method now returns a Result instead (breaking change). + Fixes Balances pallet active_issuance possible 'underflow' + Refactors the conformance test file structure to match the fungible file structure: tests for traits in regular.rs go into a test file named regular.rs, tests for traits in freezes.rs go into a test file named freezes.rs, etc. + Improve doc comments + Simplify macros + +crates: + - name: pallet-balances + - name: frame-support diff --git a/substrate/frame/assets/src/tests/sets.rs b/substrate/frame/assets/src/tests/sets.rs index bdff5175185f..f85a736c0832 100644 --- a/substrate/frame/assets/src/tests/sets.rs +++ b/substrate/frame/assets/src/tests/sets.rs @@ -153,7 +153,7 @@ fn pair_from_set_types_works() { assert_eq!(First::::total_issuance(()), 100); assert_eq!(First::::total_issuance(()), Assets::total_issuance(asset1)); - let (debt, credit) = First::::pair((), 100); + let (debt, credit) = First::::pair((), 100).unwrap(); assert_eq!(First::::total_issuance(()), 100); assert_eq!(debt.peek(), 100); assert_eq!(credit.peek(), 100); diff --git a/substrate/frame/balances/src/impl_fungible.rs b/substrate/frame/balances/src/impl_fungible.rs index 6737727e0a29..0f4e51f35012 100644 --- a/substrate/frame/balances/src/impl_fungible.rs +++ b/substrate/frame/balances/src/impl_fungible.rs @@ -177,7 +177,10 @@ impl, I: 'static> fungible::Unbalanced for Pallet::mutate(|b| b.saturating_accrue(amount)); + InactiveIssuance::::mutate(|b| { + // InactiveIssuance cannot be greater than TotalIssuance. + *b = b.saturating_add(amount).min(TotalIssuance::::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 6262aa04dc08..5c0c19a554a1 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 = ::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, ::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, ::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, + ::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 56166436003f..005674088dd3 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 @@ -16,3 +16,4 @@ // limitations under the License. pub mod inspect_mutate; +pub mod regular; diff --git a/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular/balanced.rs b/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular/balanced.rs new file mode 100644 index 000000000000..d8d20543e3d6 --- /dev/null +++ b/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular/balanced.rs @@ -0,0 +1,292 @@ +// 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::{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() +where + T: Balanced, + >::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 = 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 = 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 = 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() +where + T: Balanced, + >::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 = 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 = 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 = 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() +where + T: Balanced, + >::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() +where + T: Balanced, + >::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() +where + T: Balanced, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: AtLeast8BitUnsigned, +{ + T::set_total_issuance(50.into()); + + // Pair zero balance works + let (credit, debt) = T::pair(0.into()).unwrap(); + 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).unwrap(); + assert_eq!(credit.peek(), balance); + assert_eq!(debt.peek(), balance); + + // Creating a pair that could increase total_issuance beyond the max value returns an error + let max_value = T::Balance::max_value(); + let distance_from_max_value = 5.into(); + T::set_total_issuance(max_value - distance_from_max_value); + T::pair(distance_from_max_value + 5.into()).unwrap_err(); +} diff --git a/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular/mod.rs b/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular/mod.rs new file mode 100644 index 000000000000..85acbcf2fcd3 --- /dev/null +++ b/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular/mod.rs @@ -0,0 +1,20 @@ +// 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 balanced; +pub mod mutate; +pub mod unbalanced; diff --git a/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular/mutate.rs b/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular/mutate.rs new file mode 100644 index 000000000000..95b5256bb491 --- /dev/null +++ b/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular/mutate.rs @@ -0,0 +1,783 @@ +// 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 [`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() +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 [`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() +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 [`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() +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 [`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() +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 [`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() +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 [`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() +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 [`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() +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 [`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() +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 [`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() +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 [`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() +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 [`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() +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 [`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() +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 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() +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 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(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 [`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() +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 [`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() +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 [`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() +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 [`Inspect::can_deposit`] works correctly returns [`DepositConsequence::Success`] +/// when depositing an amount that should succeed. +pub fn can_deposit_success() +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 [`Inspect::can_deposit`] returns [`DepositConsequence::BelowMinimum`] when depositing +/// below the minimum balance. +pub fn can_deposit_below_minimum() +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 [`Inspect::can_deposit`] returns [`DepositConsequence::Overflow`] when +/// depositing an amount that would overflow. +pub fn can_deposit_overflow() +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 [`Inspect::can_withdraw`] returns [`WithdrawConsequence::Success`] when withdrawing an +/// amount that should succeed. +pub fn can_withdraw_success() +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 [`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() +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 [`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() +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 [`Inspect::reducible_balance`] returns the full account balance when called with +/// [`Preservation::Expendable`]. +pub fn reducible_balance_expendable() +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); +} + +/// 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() +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/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular/unbalanced.rs b/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular/unbalanced.rs new file mode 100644 index 000000000000..e7fcc15472e0 --- /dev/null +++ b/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular/unbalanced.rs @@ -0,0 +1,281 @@ +// 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, 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() +where + T: Unbalanced, + >::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() +where + T: Unbalanced, + >::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() +where + T: Unbalanced, + >::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::FundsUnavailable.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() +where + T: Unbalanced, + >::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() +where + T: Unbalanced, + >::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() +where + T: Unbalanced, + >::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()); +} 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 fe252c6b0893..37749d396009 100644 --- a/substrate/frame/support/src/traits/tokens/fungible/item_of.rs +++ b/substrate/frame/support/src/traits/tokens/fungible/item_of.rs @@ -389,9 +389,11 @@ impl< let credit = >::issue(A::get(), amount); imbalance::from_fungibles(credit) } - fn pair(amount: Self::Balance) -> (Debt, Credit) { - let (a, b) = >::pair(A::get(), amount); - (imbalance::from_fungibles(a), imbalance::from_fungibles(b)) + fn pair( + amount: Self::Balance, + ) -> Result<(Debt, Credit), DispatchError> { + let (a, b) = >::pair(A::get(), amount)?; + Ok((imbalance::from_fungibles(a), imbalance::from_fungibles(b))) } fn rescind(amount: Self::Balance) -> Debt { let debt = >::rescind(A::get(), amount); diff --git a/substrate/frame/support/src/traits/tokens/fungible/regular.rs b/substrate/frame/support/src/traits/tokens/fungible/regular.rs index 0109c349eae3..5060dc3e24a8 100644 --- a/substrate/frame/support/src/traits/tokens/fungible/regular.rs +++ b/substrate/frame/support/src/traits/tokens/fungible/regular.rs @@ -64,7 +64,7 @@ pub trait Inspect: 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()`. @@ -74,7 +74,7 @@ pub trait Inspect: 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 @@ -82,7 +82,7 @@ pub trait Inspect: 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, @@ -106,7 +106,7 @@ pub trait Inspect: Sized { fn can_withdraw(who: &AccountId, amount: Self::Balance) -> WithdrawConsequence; } -/// 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>(pub T::Balance); @@ -123,20 +123,20 @@ impl> Dust { /// 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: Inspect { - /// 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); /// Forcefully set the balance of `who` to `amount`. @@ -151,9 +151,10 @@ pub trait Unbalanced: Inspect { /// 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, @@ -164,14 +165,14 @@ pub trait Unbalanced: Inspect { /// 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, @@ -180,15 +181,10 @@ pub trait Unbalanced: Inspect { force: Fortitude, ) -> Result { let old_balance = Self::balance(who); - let free = Self::reducible_balance(who, preservation, force); + let reducible = Self::reducible_balance(who, preservation, force); match precision { - BestEffort => { - amount = amount.min(free); - }, - Exact => - if free < amount { - return Err(TokenError::FundsUnavailable.into()) - }, + BestEffort => amount = amount.min(reducible), + Exact => ensure!(reducible >= amount, TokenError::FundsUnavailable), } let new_balance = old_balance.checked_sub(&amount).ok_or(TokenError::FundsUnavailable)?; @@ -203,7 +199,7 @@ pub trait Unbalanced: Inspect { /// 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, @@ -276,8 +272,8 @@ where /// 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). @@ -296,8 +292,8 @@ where /// 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). @@ -325,7 +321,7 @@ where let _extra = Self::can_withdraw(source, amount).into_result(preservation != Expendable)?; Self::can_deposit(dest, amount, Extant).into_result()?; if source == dest { - return Ok(amount) + return Ok(amount); } Self::decrease_balance(source, amount, BestEffort, preservation, Polite)?; @@ -383,7 +379,7 @@ impl> HandleImbalanceDrop /// 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: Inspect + Unbalanced { /// The type for managing what happens when an instance of `Debt` is dropped without being used. type OnDropDebt: HandleImbalanceDrop; @@ -392,7 +388,7 @@ pub trait Balanced: Inspect + Unbalanced { type OnDropCredit: HandleImbalanceDrop; /// 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. @@ -407,7 +403,7 @@ pub trait Balanced: Inspect + Unbalanced { /// 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. @@ -424,18 +420,33 @@ pub trait Balanced: Inspect + Unbalanced { /// /// 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, Credit) { - (Self::rescind(amount), Self::issue(amount)) + /// + /// This could fail when we cannot issue and redeem the entire `amount`, for example in the + /// case where the amount would cause overflow or underflow in [`Balanced::issue`] or + /// [`Balanced::rescind`]. + fn pair( + amount: Self::Balance, + ) -> Result<(Debt, Credit), DispatchError> { + let issued = Self::issue(amount); + let rescinded = Self::rescind(amount); + // Need to check amount in case by some edge case both issued and rescinded are below + // `amount` by the exact same value + if issued.peek() != rescinded.peek() || issued.peek() != amount { + // Issued and rescinded will be dropped automatically + Err("Failed to issue and rescind equal amounts".into()) + } else { + Ok((rescinded, issued)) + } } /// 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, @@ -449,8 +460,8 @@ pub trait Balanced: Inspect + Unbalanced { /// 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. /// @@ -458,7 +469,7 @@ pub trait Balanced: Inspect + Unbalanced { /// 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, @@ -476,7 +487,7 @@ pub trait Balanced: Inspect + Unbalanced { /// 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, @@ -503,7 +514,7 @@ pub trait Balanced: Inspect + Unbalanced { 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) { diff --git a/substrate/frame/support/src/traits/tokens/fungible/union_of.rs b/substrate/frame/support/src/traits/tokens/fungible/union_of.rs index 86505befc05f..33711d7a16cc 100644 --- a/substrate/frame/support/src/traits/tokens/fungible/union_of.rs +++ b/substrate/frame/support/src/traits/tokens/fungible/union_of.rs @@ -727,21 +727,22 @@ impl< fn pair( asset: Self::AssetId, amount: Self::Balance, - ) -> (fungibles::Debt, fungibles::Credit) { + ) -> Result<(fungibles::Debt, fungibles::Credit), DispatchError> + { match Criterion::convert(asset.clone()) { Left(()) => { - let (a, b) = >::pair(amount); - ( + let (a, b) = >::pair(amount)?; + Ok(( fungibles::imbalance::from_fungible(a, asset.clone()), fungibles::imbalance::from_fungible(b, asset), - ) + )) }, Right(a) => { - let (a, b) = >::pair(a, amount); - ( + let (a, b) = >::pair(a, amount)?; + Ok(( fungibles::imbalance::from_fungibles(a, asset.clone()), fungibles::imbalance::from_fungibles(b, asset), - ) + )) }, } } diff --git a/substrate/frame/support/src/traits/tokens/fungibles/regular.rs b/substrate/frame/support/src/traits/tokens/fungibles/regular.rs index a2fc4e550952..08e43b75aa69 100644 --- a/substrate/frame/support/src/traits/tokens/fungibles/regular.rs +++ b/substrate/frame/support/src/traits/tokens/fungibles/regular.rs @@ -194,9 +194,10 @@ pub trait Unbalanced: Inspect { force: Fortitude, ) -> Result { let old_balance = Self::balance(asset.clone(), who); - let free = Self::reducible_balance(asset.clone(), who, preservation, force); - if let BestEffort = precision { - amount = amount.min(free); + let reducible = Self::reducible_balance(asset.clone(), who, preservation, force); + match precision { + BestEffort => amount = amount.min(reducible), + Exact => ensure!(reducible >= amount, TokenError::FundsUnavailable), } let new_balance = old_balance.checked_sub(&amount).ok_or(TokenError::FundsUnavailable)?; if let Some(dust) = Self::write_balance(asset.clone(), who, new_balance)? { @@ -478,11 +479,24 @@ pub trait Balanced: Inspect + Unbalanced { /// /// This is just the same as burning and issuing the same amount and has no effect on the /// total issuance. + /// + /// This is infallible, but doesn't guarantee that the entire `amount` is used to create the + /// pair, for example in the case where the amounts would cause overflow or underflow in + /// [`Balanced::issue`] or [`Balanced::rescind`]. fn pair( asset: Self::AssetId, amount: Self::Balance, - ) -> (Debt, Credit) { - (Self::rescind(asset.clone(), amount), Self::issue(asset, amount)) + ) -> Result<(Debt, Credit), DispatchError> { + let issued = Self::issue(asset.clone(), amount); + let rescinded = Self::rescind(asset, amount); + // Need to check amount in case by some edge case both issued and rescinded are below + // `amount` by the exact same value + if issued.peek() != rescinded.peek() || issued.peek() != amount { + // Issued and rescinded will be dropped automatically + Err("Failed to issue and rescind equal amounts".into()) + } else { + Ok((rescinded, issued)) + } } /// Mints `value` into the account of `who`, creating it as needed. diff --git a/substrate/frame/support/src/traits/tokens/fungibles/union_of.rs b/substrate/frame/support/src/traits/tokens/fungibles/union_of.rs index 3619db3a37b6..9d2a783df2a4 100644 --- a/substrate/frame/support/src/traits/tokens/fungibles/union_of.rs +++ b/substrate/frame/support/src/traits/tokens/fungibles/union_of.rs @@ -706,15 +706,22 @@ impl< fn pair( asset: Self::AssetId, amount: Self::Balance, - ) -> (fungibles::Debt, fungibles::Credit) { + ) -> Result<(fungibles::Debt, fungibles::Credit), DispatchError> + { match Criterion::convert(asset.clone()) { Left(a) => { - let (a, b) = >::pair(a, amount); - (imbalance::from_fungibles(a, asset.clone()), imbalance::from_fungibles(b, asset)) + let (a, b) = >::pair(a, amount)?; + Ok(( + imbalance::from_fungibles(a, asset.clone()), + imbalance::from_fungibles(b, asset), + )) }, Right(a) => { - let (a, b) = >::pair(a, amount); - (imbalance::from_fungibles(a, asset.clone()), imbalance::from_fungibles(b, asset)) + let (a, b) = >::pair(a, amount)?; + Ok(( + imbalance::from_fungibles(a, asset.clone()), + imbalance::from_fungibles(b, asset), + )) }, } }