From f8a1dd4c836d7d29382ba66fc2fce40fc1364546 Mon Sep 17 00:00:00 2001 From: Gavin Wood Date: Sat, 18 Mar 2023 14:47:55 +0000 Subject: [PATCH] Deprecate `Currency`; introduce holds and freezing into `fungible` traits (#12951) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * First reworking of fungibles API * New API and docs * More fungible::* API improvements * New ref-counting logic for old API * Missing files * Fixes * Use the new transfer logic * Use fungibles for the dispatchables * Use shelve/restore names * Locking works with total balance. * repotting and removal * Separate Holds from Reserves * Introduce freezes * Missing files * Tests for freezing * Fix hold+freeze combo * More tests * Fee-free dispatchable for upgrading accounts * Benchmarks and a few fixes * Another test * Docs and refactor to avoid blanket impls * Repot * Fit out ItemOf fully * Add events to Balanced traits * Introduced events into Hold traits * Fix Assets pallet tests * Assets benchmarks pass * Missing files and fixes * Fixes * Fixes * Benchmarks fixes * Fix balance benchmarks * Formatting * Expose fungible sub modules * Move NIS to fungible API * Fix broken impl and add test * Fix tests * API for `transfer_and_hold` * Use composite APIs * Formatting * Upgraded event * Fixes * Fixes * Fixes * Fixes * Repot tests and some fixed * Fix some bits * Fix dust tests * Rename `set_balance` - `Balances::set_balance` becomes `Balances::force_set_balance` - `Unbalanced::set_balance` becomes `Unbalances::write_balance` * becomes * Move dust handling to fungibles API * Formatting * Fixes and more refactoring * Fixes * Fixes * Fixes * Fixes * Fixes * Fixes * Fixes * Fixes * Fixes * Use reducible_balance for better correctness on fees * Reducing hold to zero should remove entry. * Add test * Docs * Update frame/support/src/traits/tokens/fungibles/hold.rs Co-authored-by: Muharem Ismailov * Update frame/support/src/traits/tokens/fungibles/regular.rs Co-authored-by: Muharem Ismailov * Update frame/support/src/traits/tokens/fungible/hold.rs Co-authored-by: Muharem Ismailov * Update frame/support/src/traits/tokens/fungible/regular.rs Co-authored-by: Muharem Ismailov * Docs * Docs * Docs * Fix NIS benchmarks * Doc comment * Remove post_mutation * Fix some tests * Fix some grumbles * Enumify bool args to fungible(s) functions * Fix up assets and balances * Formatting * Fix contracts * Fix tests & benchmarks build * Typify minted boolean arg * Typify on_hold boolean arg; renames * Fix numerous tests * Fix dependency issue * Privatize dangerous API mutate_account * Fix contracts (@alext - please check this commit) * Remove println * Fix tests for contracts * Fix broken rename * Fix broken rename * Fix broken rename * Docs * Update frame/support/src/traits/tokens/fungible/hold.rs Co-authored-by: Anthony Alaribe * remove from_ref_time * Update frame/executive/src/lib.rs Co-authored-by: Anthony Alaribe * Update frame/executive/src/lib.rs Co-authored-by: Anthony Alaribe * Reenable test * Update frame/support/src/traits/tokens/fungibles/hold.rs Co-authored-by: Anthony Alaribe * Update frame/support/src/traits/tokens/fungible/hold.rs Co-authored-by: Anthony Alaribe * Update frame/support/src/traits/tokens/fungible/hold.rs Co-authored-by: Anthony Alaribe * Update frame/support/src/traits/tokens/fungible/hold.rs Co-authored-by: Anthony Alaribe * Update frame/support/src/traits/tokens/currency.rs Co-authored-by: Anthony Alaribe * Update frame/lottery/src/tests.rs Co-authored-by: Anthony Alaribe * Update frame/support/src/traits/tokens/fungible/mod.rs Co-authored-by: Anthony Alaribe * Update frame/support/src/traits/tokens/fungible/regular.rs Co-authored-by: Anthony Alaribe * Update frame/support/src/traits/tokens/fungibles/freeze.rs Co-authored-by: Anthony Alaribe * Update frame/support/src/traits/tokens/fungible/regular.rs Co-authored-by: Anthony Alaribe * Update frame/support/src/traits/tokens/fungibles/hold.rs Co-authored-by: Anthony Alaribe * Update frame/support/src/traits/tokens/fungibles/hold.rs Co-authored-by: Anthony Alaribe * Update frame/support/src/traits/tokens/fungibles/hold.rs Co-authored-by: Anthony Alaribe * Rename UnwantedRemoval to UnwantedAccountRemoval * Docs * Formatting * Update frame/balances/src/lib.rs Co-authored-by: Oliver Tale-Yazdi * Update primitives/runtime/src/lib.rs Co-authored-by: Keith Yeung * handle_raw_dust oes nothing * Formatting * Fixes * Grumble * Fixes * Add test * Add test * Tests for reducible_balance * Fixes * Fix Salary * Fixes * Disable broken test * Disable nicely * Fixes * Fixes * Fixes * Rename some events * Fix nomination pools breakage * Add compatibility stub for transfer tx * Reinstate a safely compatible version of Balances set_balance * Fixes * Grumble * Update frame/nis/src/lib.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * ".git/.scripts/commands/bench/bench.sh" pallet dev pallet_balances * disable flakey tests * Update frame/balances/src/lib.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * Grumbles * Grumble --------- Co-authored-by: Muharem Ismailov Co-authored-by: Alexander Theißen Co-authored-by: Anthony Alaribe Co-authored-by: Oliver Tale-Yazdi Co-authored-by: Keith Yeung Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Co-authored-by: command-bot <> --- bin/node-template/runtime/src/lib.rs | 4 + bin/node/cli/benches/block_production.rs | 2 +- bin/node/cli/benches/transaction_pool.rs | 8 +- bin/node/cli/src/service.rs | 2 +- bin/node/cli/tests/temp_base_path_works.rs | 4 +- bin/node/executor/benches/bench.rs | 2 +- bin/node/executor/tests/basic.rs | 57 +- bin/node/executor/tests/common.rs | 5 +- bin/node/executor/tests/fees.rs | 6 +- bin/node/executor/tests/submit_transaction.rs | 12 +- bin/node/runtime/src/impls.rs | 4 +- bin/node/runtime/src/lib.rs | 21 +- bin/node/testing/src/bench.rs | 2 +- frame/alliance/src/mock.rs | 4 + frame/assets/src/benchmarking.rs | 8 +- frame/assets/src/functions.rs | 15 +- frame/assets/src/impl_fungibles.rs | 126 +- frame/assets/src/lib.rs | 6 +- frame/assets/src/mock.rs | 6 +- frame/assets/src/tests.rs | 30 +- frame/atomic-swap/src/tests.rs | 4 + frame/babe/src/mock.rs | 4 + frame/balances/README.md | 2 +- frame/balances/src/benchmarking.rs | 74 +- frame/balances/src/impl_currency.rs | 898 +++++++ frame/balances/src/impl_fungible.rs | 358 +++ frame/balances/src/lib.rs | 2387 +++++------------ frame/balances/src/tests.rs | 1460 ---------- frame/balances/src/tests/currency_tests.rs | 1231 +++++++++ .../balances/src/tests/dispatchable_tests.rs | 224 ++ frame/balances/src/tests/fungible_tests.rs | 399 +++ frame/balances/src/tests/mod.rs | 296 ++ frame/balances/src/tests/reentrancy_tests.rs | 195 ++ frame/balances/src/tests_composite.rs | 149 - frame/balances/src/tests_local.rs | 191 -- frame/balances/src/tests_reentrancy.rs | 264 -- frame/balances/src/types.rs | 157 ++ frame/balances/src/weights.rs | 49 +- frame/beefy/src/mock.rs | 4 + frame/bounties/src/benchmarking.rs | 7 +- frame/bounties/src/tests.rs | 8 +- frame/child-bounties/src/benchmarking.rs | 12 +- frame/child-bounties/src/tests.rs | 8 +- frame/contracts/src/benchmarking/mod.rs | 12 +- frame/contracts/src/exec.rs | 15 +- frame/contracts/src/lib.rs | 2 +- frame/contracts/src/storage/meter.rs | 7 +- frame/contracts/src/tests.rs | 73 +- frame/conviction-voting/src/benchmarking.rs | 12 +- frame/conviction-voting/src/lib.rs | 7 +- frame/conviction-voting/src/tests.rs | 6 +- frame/democracy/src/tests.rs | 8 +- .../src/benchmarking.rs | 10 +- .../election-provider-multi-phase/src/mock.rs | 4 + frame/elections-phragmen/src/lib.rs | 7 +- frame/examples/basic/src/tests.rs | 4 + frame/executive/src/lib.rs | 100 +- frame/fast-unstake/src/mock.rs | 4 + frame/grandpa/src/mock.rs | 4 + frame/identity/src/tests.rs | 4 + frame/indices/src/mock.rs | 4 + frame/lottery/src/mock.rs | 4 + frame/lottery/src/tests.rs | 77 +- frame/multisig/src/tests.rs | 72 +- frame/nfts/src/mock.rs | 4 + frame/nicks/src/lib.rs | 4 + frame/nis/README.md | 5 +- frame/nis/src/benchmarking.rs | 102 +- frame/nis/src/lib.rs | 240 +- frame/nis/src/mock.rs | 50 +- frame/nis/src/tests.rs | 110 +- .../nomination-pools/benchmarking/src/mock.rs | 4 + frame/nomination-pools/src/mock.rs | 4 + frame/nomination-pools/src/tests.rs | 177 +- .../nomination-pools/test-staking/src/mock.rs | 4 + frame/offences/benchmarking/src/mock.rs | 4 + frame/preimage/src/mock.rs | 4 + frame/proxy/src/tests.rs | 28 +- frame/recovery/src/mock.rs | 4 + frame/recovery/src/tests.rs | 10 +- frame/referenda/src/migration.rs | 2 +- frame/referenda/src/mock.rs | 17 +- frame/root-offences/src/mock.rs | 4 + frame/scored-pool/src/mock.rs | 4 + frame/session/benchmarking/src/mock.rs | 4 + frame/society/src/mock.rs | 4 + frame/staking/src/mock.rs | 4 + frame/staking/src/tests.rs | 22 +- frame/state-trie-migration/src/lib.rs | 4 + frame/support/src/traits/stored_map.rs | 35 +- frame/support/src/traits/tokens.rs | 3 +- frame/support/src/traits/tokens/fungible.rs | 367 --- .../src/traits/tokens/fungible/balanced.rs | 354 --- .../src/traits/tokens/fungible/freeze.rs | 68 + .../src/traits/tokens/fungible/hold.rs | 393 +++ .../src/traits/tokens/fungible/imbalance.rs | 15 +- .../src/traits/tokens/fungible/item_of.rs | 451 ++++ .../support/src/traits/tokens/fungible/mod.rs | 56 + .../src/traits/tokens/fungible/regular.rs | 506 ++++ frame/support/src/traits/tokens/fungibles.rs | 332 --- .../src/traits/tokens/fungibles/balanced.rs | 394 --- .../src/traits/tokens/fungibles/enumerable.rs | 4 +- .../src/traits/tokens/fungibles/freeze.rs | 78 + .../src/traits/tokens/fungibles/hold.rs | 457 ++++ .../src/traits/tokens/fungibles/imbalance.rs | 13 +- .../src/traits/tokens/fungibles/lifetime.rs | 84 + .../src/traits/tokens/fungibles/mod.rs | 40 + .../src/traits/tokens/fungibles/regular.rs | 571 ++++ frame/support/src/traits/tokens/misc.rs | 62 +- frame/support/src/traits/tokens/pay.rs | 8 +- frame/system/src/lib.rs | 23 +- frame/system/src/tests.rs | 2 + frame/tips/src/tests.rs | 4 + .../asset-tx-payment/src/lib.rs | 8 +- .../asset-tx-payment/src/mock.rs | 6 +- .../asset-tx-payment/src/payment.rs | 23 +- .../asset-tx-payment/src/tests.rs | 2 +- frame/transaction-payment/src/mock.rs | 6 +- frame/transaction-payment/src/tests.rs | 10 +- frame/transaction-storage/src/mock.rs | 4 + frame/treasury/src/tests.rs | 4 + frame/uniques/src/mock.rs | 4 + frame/utility/Cargo.toml | 1 + frame/utility/src/tests.rs | 17 +- frame/vesting/src/benchmarking.rs | 12 +- frame/vesting/src/mock.rs | 4 + frame/vesting/src/tests.rs | 37 +- frame/whitelist/src/mock.rs | 4 + primitives/runtime/src/lib.rs | 22 +- 129 files changed, 8325 insertions(+), 6119 deletions(-) create mode 100644 frame/balances/src/impl_currency.rs create mode 100644 frame/balances/src/impl_fungible.rs delete mode 100644 frame/balances/src/tests.rs create mode 100644 frame/balances/src/tests/currency_tests.rs create mode 100644 frame/balances/src/tests/dispatchable_tests.rs create mode 100644 frame/balances/src/tests/fungible_tests.rs create mode 100644 frame/balances/src/tests/mod.rs create mode 100644 frame/balances/src/tests/reentrancy_tests.rs delete mode 100644 frame/balances/src/tests_composite.rs delete mode 100644 frame/balances/src/tests_local.rs delete mode 100644 frame/balances/src/tests_reentrancy.rs create mode 100644 frame/balances/src/types.rs delete mode 100644 frame/support/src/traits/tokens/fungible.rs delete mode 100644 frame/support/src/traits/tokens/fungible/balanced.rs create mode 100644 frame/support/src/traits/tokens/fungible/freeze.rs create mode 100644 frame/support/src/traits/tokens/fungible/hold.rs create mode 100644 frame/support/src/traits/tokens/fungible/item_of.rs create mode 100644 frame/support/src/traits/tokens/fungible/mod.rs create mode 100644 frame/support/src/traits/tokens/fungible/regular.rs delete mode 100644 frame/support/src/traits/tokens/fungibles.rs delete mode 100644 frame/support/src/traits/tokens/fungibles/balanced.rs create mode 100644 frame/support/src/traits/tokens/fungibles/freeze.rs create mode 100644 frame/support/src/traits/tokens/fungibles/hold.rs create mode 100644 frame/support/src/traits/tokens/fungibles/lifetime.rs create mode 100644 frame/support/src/traits/tokens/fungibles/mod.rs create mode 100644 frame/support/src/traits/tokens/fungibles/regular.rs diff --git a/bin/node-template/runtime/src/lib.rs b/bin/node-template/runtime/src/lib.rs index 41871c06bd..50bcb67cb4 100644 --- a/bin/node-template/runtime/src/lib.rs +++ b/bin/node-template/runtime/src/lib.rs @@ -243,6 +243,10 @@ impl pallet_balances::Config for Runtime { type ExistentialDeposit = ConstU128; type AccountStore = System; type WeightInfo = pallet_balances::weights::SubstrateWeight; + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } parameter_types! { diff --git a/bin/node/cli/benches/block_production.rs b/bin/node/cli/benches/block_production.rs index 501d69fc28..f1c65955ea 100644 --- a/bin/node/cli/benches/block_production.rs +++ b/bin/node/cli/benches/block_production.rs @@ -162,7 +162,7 @@ fn prepare_benchmark(client: &FullClient) -> (usize, Vec) { let extrinsic: OpaqueExtrinsic = create_extrinsic( client, src.clone(), - BalancesCall::transfer { dest: dst.clone(), value: 1 * DOLLARS }, + BalancesCall::transfer_allow_death { dest: dst.clone(), value: 1 * DOLLARS }, Some(nonce), ) .into(); diff --git a/bin/node/cli/benches/transaction_pool.rs b/bin/node/cli/benches/transaction_pool.rs index 165c67f602..72c1c81608 100644 --- a/bin/node/cli/benches/transaction_pool.rs +++ b/bin/node/cli/benches/transaction_pool.rs @@ -140,10 +140,9 @@ fn create_account_extrinsics( Sr25519Keyring::Alice.pair(), SudoCall::sudo { call: Box::new( - BalancesCall::set_balance { + BalancesCall::force_set_balance { who: AccountId::from(a.public()).into(), new_free: 0, - new_reserved: 0, } .into(), ), @@ -156,10 +155,9 @@ fn create_account_extrinsics( Sr25519Keyring::Alice.pair(), SudoCall::sudo { call: Box::new( - BalancesCall::set_balance { + BalancesCall::force_set_balance { who: AccountId::from(a.public()).into(), new_free: 1_000_000 * DOLLARS, - new_reserved: 0, } .into(), ), @@ -184,7 +182,7 @@ fn create_benchmark_extrinsics( create_extrinsic( client, account.clone(), - BalancesCall::transfer { + BalancesCall::transfer_allow_death { dest: Sr25519Keyring::Bob.to_account_id().into(), value: 1 * DOLLARS, }, diff --git a/bin/node/cli/src/service.rs b/bin/node/cli/src/service.rs index a2d66cd634..d32223339d 100644 --- a/bin/node/cli/src/service.rs +++ b/bin/node/cli/src/service.rs @@ -772,7 +772,7 @@ mod tests { }; let signer = charlie.clone(); - let function = RuntimeCall::Balances(BalancesCall::transfer { + let function = RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: to.into(), value: amount, }); diff --git a/bin/node/cli/tests/temp_base_path_works.rs b/bin/node/cli/tests/temp_base_path_works.rs index 89e901c00e..bfc9f88220 100644 --- a/bin/node/cli/tests/temp_base_path_works.rs +++ b/bin/node/cli/tests/temp_base_path_works.rs @@ -26,7 +26,9 @@ use std::{ pub mod common; -#[tokio::test] +#[allow(dead_code)] +// Apparently `#[ignore]` doesn't actually work to disable this one. +//#[tokio::test] async fn temp_base_path_works() { common::run_with_timeout(Duration::from_secs(60 * 10), async move { let mut cmd = Command::new(cargo_bin("substrate")); diff --git a/bin/node/executor/benches/bench.rs b/bin/node/executor/benches/bench.rs index 19e7b158a8..a8c31068e8 100644 --- a/bin/node/executor/benches/bench.rs +++ b/bin/node/executor/benches/bench.rs @@ -167,7 +167,7 @@ fn test_blocks( }]; block1_extrinsics.extend((0..20).map(|i| CheckedExtrinsic { signed: Some((alice(), signed_extra(i, 0))), - function: RuntimeCall::Balances(pallet_balances::Call::transfer { + function: RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { dest: bob().into(), value: 1 * DOLLARS, }), diff --git a/bin/node/executor/tests/basic.rs b/bin/node/executor/tests/basic.rs index 9200fe735b..d301aa06f9 100644 --- a/bin/node/executor/tests/basic.rs +++ b/bin/node/executor/tests/basic.rs @@ -89,7 +89,7 @@ fn changes_trie_block() -> (Vec, Hash) { }, CheckedExtrinsic { signed: Some((alice(), signed_extra(0, 0))), - function: RuntimeCall::Balances(pallet_balances::Call::transfer { + function: RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { dest: bob().into(), value: 69 * DOLLARS, }), @@ -116,7 +116,7 @@ fn blocks() -> ((Vec, Hash), (Vec, Hash)) { }, CheckedExtrinsic { signed: Some((alice(), signed_extra(0, 0))), - function: RuntimeCall::Balances(pallet_balances::Call::transfer { + function: RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { dest: bob().into(), value: 69 * DOLLARS, }), @@ -136,14 +136,14 @@ fn blocks() -> ((Vec, Hash), (Vec, Hash)) { }, CheckedExtrinsic { signed: Some((bob(), signed_extra(0, 0))), - function: RuntimeCall::Balances(pallet_balances::Call::transfer { + function: RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { dest: alice().into(), value: 5 * DOLLARS, }), }, CheckedExtrinsic { signed: Some((alice(), signed_extra(1, 0))), - function: RuntimeCall::Balances(pallet_balances::Call::transfer { + function: RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { dest: bob().into(), value: 15 * DOLLARS, }), @@ -183,7 +183,12 @@ fn panic_execution_with_foreign_code_gives_error() { let mut t = new_test_ext(bloaty_code_unwrap()); t.insert( >::hashed_key_for(alice()), - (69u128, 0u32, 0u128, 0u128, 0u128).encode(), + AccountInfo::<::Index, _> { + providers: 1, + data: (69u128, 0u128, 0u128, 1u128 << 127), + ..Default::default() + } + .encode(), ); t.insert(>::hashed_key().to_vec(), 69_u128.encode()); t.insert(>::hashed_key_for(0), vec![0u8; 32]); @@ -204,9 +209,14 @@ fn bad_extrinsic_with_native_equivalent_code_gives_error() { let mut t = new_test_ext(compact_code_unwrap()); t.insert( >::hashed_key_for(alice()), - (0u32, 0u32, 0u32, 69u128, 0u128, 0u128, 0u128).encode(), + AccountInfo::<::Index, _> { + providers: 1, + data: (69u128, 0u128, 0u128, 1u128 << 127), + ..Default::default() + } + .encode(), ); - t.insert(>::hashed_key().to_vec(), 69_u128.encode()); + t.insert(>::hashed_key().to_vec(), 69u128.encode()); t.insert(>::hashed_key_for(0), vec![0u8; 32]); let r = @@ -226,17 +236,18 @@ fn successful_execution_with_native_equivalent_code_gives_ok() { t.insert( >::hashed_key_for(alice()), AccountInfo::<::Index, _> { - data: (111 * DOLLARS, 0u128, 0u128, 0u128), + providers: 1, + data: (111 * DOLLARS, 0u128, 0u128, 1u128 << 127), ..Default::default() } .encode(), ); t.insert( >::hashed_key_for(bob()), - AccountInfo::<::Index, _> { - data: (0 * DOLLARS, 0u128, 0u128, 0u128), - ..Default::default() - } + AccountInfo::< + ::Index, + ::AccountData, + >::default() .encode(), ); t.insert( @@ -267,17 +278,18 @@ fn successful_execution_with_foreign_code_gives_ok() { t.insert( >::hashed_key_for(alice()), AccountInfo::<::Index, _> { - data: (111 * DOLLARS, 0u128, 0u128, 0u128), + providers: 1, + data: (111 * DOLLARS, 0u128, 0u128, 1u128 << 127), ..Default::default() } .encode(), ); t.insert( >::hashed_key_for(bob()), - AccountInfo::<::Index, _> { - data: (0 * DOLLARS, 0u128, 0u128, 0u128), - ..Default::default() - } + AccountInfo::< + ::Index, + ::AccountData, + >::default() .encode(), ); t.insert( @@ -784,17 +796,18 @@ fn successful_execution_gives_ok() { t.insert( >::hashed_key_for(alice()), AccountInfo::<::Index, _> { - data: (111 * DOLLARS, 0u128, 0u128, 0u128), + providers: 1, + data: (111 * DOLLARS, 0u128, 0u128, 1u128 << 127), ..Default::default() } .encode(), ); t.insert( >::hashed_key_for(bob()), - AccountInfo::<::Index, _> { - data: (0 * DOLLARS, 0u128, 0u128, 0u128), - ..Default::default() - } + AccountInfo::< + ::Index, + ::AccountData, + >::default() .encode(), ); t.insert( diff --git a/bin/node/executor/tests/common.rs b/bin/node/executor/tests/common.rs index 036528f8da..2f92423ffb 100644 --- a/bin/node/executor/tests/common.rs +++ b/bin/node/executor/tests/common.rs @@ -87,7 +87,10 @@ pub fn sign(xt: CheckedExtrinsic) -> UncheckedExtrinsic { } pub fn default_transfer_call() -> pallet_balances::Call { - pallet_balances::Call::::transfer { dest: bob().into(), value: 69 * DOLLARS } + pallet_balances::Call::::transfer_allow_death { + dest: bob().into(), + value: 69 * DOLLARS, + } } pub fn from_block_number(n: u32) -> Header { diff --git a/bin/node/executor/tests/fees.rs b/bin/node/executor/tests/fees.rs index cbf0fdce94..970d790a87 100644 --- a/bin/node/executor/tests/fees.rs +++ b/bin/node/executor/tests/fees.rs @@ -120,9 +120,9 @@ fn new_account_info(free_dollars: u128) -> Vec { frame_system::AccountInfo { nonce: 0u32, consumers: 0, - providers: 0, + providers: 1, sufficients: 0, - data: (free_dollars * DOLLARS, 0 * DOLLARS, 0 * DOLLARS, 0 * DOLLARS), + data: (free_dollars * DOLLARS, 0 * DOLLARS, 0 * DOLLARS, 1u128 << 127), } .encode() } @@ -214,7 +214,7 @@ fn block_weight_capacity_report() { let mut xts = (0..num_transfers) .map(|i| CheckedExtrinsic { signed: Some((charlie(), signed_extra(nonce + i as Index, 0))), - function: RuntimeCall::Balances(pallet_balances::Call::transfer { + function: RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { dest: bob().into(), value: 0, }), diff --git a/bin/node/executor/tests/submit_transaction.rs b/bin/node/executor/tests/submit_transaction.rs index 32eacd78b7..a3d6681fb6 100644 --- a/bin/node/executor/tests/submit_transaction.rs +++ b/bin/node/executor/tests/submit_transaction.rs @@ -86,7 +86,7 @@ fn should_submit_signed_transaction() { t.execute_with(|| { let results = Signer::::all_accounts().send_signed_transaction(|_| { - pallet_balances::Call::transfer { + pallet_balances::Call::transfer_allow_death { dest: Alice.to_account_id().into(), value: Default::default(), } @@ -123,7 +123,7 @@ fn should_submit_signed_twice_from_the_same_account() { t.execute_with(|| { let result = Signer::::any_account().send_signed_transaction(|_| { - pallet_balances::Call::transfer { + pallet_balances::Call::transfer_allow_death { dest: Alice.to_account_id().into(), value: Default::default(), } @@ -135,7 +135,7 @@ fn should_submit_signed_twice_from_the_same_account() { // submit another one from the same account. The nonce should be incremented. let result = Signer::::any_account().send_signed_transaction(|_| { - pallet_balances::Call::transfer { + pallet_balances::Call::transfer_allow_death { dest: Alice.to_account_id().into(), value: Default::default(), } @@ -174,7 +174,7 @@ fn should_submit_signed_twice_from_all_accounts() { t.execute_with(|| { let results = Signer::::all_accounts() .send_signed_transaction(|_| { - pallet_balances::Call::transfer { dest: Alice.to_account_id().into(), value: Default::default() } + pallet_balances::Call::transfer_allow_death { dest: Alice.to_account_id().into(), value: Default::default() } }); let len = results.len(); @@ -185,7 +185,7 @@ fn should_submit_signed_twice_from_all_accounts() { // submit another one from the same account. The nonce should be incremented. let results = Signer::::all_accounts() .send_signed_transaction(|_| { - pallet_balances::Call::transfer { dest: Alice.to_account_id().into(), value: Default::default() } + pallet_balances::Call::transfer_allow_death { dest: Alice.to_account_id().into(), value: Default::default() } }); let len = results.len(); @@ -238,7 +238,7 @@ fn submitted_transaction_should_be_valid() { t.execute_with(|| { let results = Signer::::all_accounts().send_signed_transaction(|_| { - pallet_balances::Call::transfer { + pallet_balances::Call::transfer_allow_death { dest: Alice.to_account_id().into(), value: Default::default(), } diff --git a/bin/node/runtime/src/impls.rs b/bin/node/runtime/src/impls.rs index 09f7b6a29e..033549549a 100644 --- a/bin/node/runtime/src/impls.rs +++ b/bin/node/runtime/src/impls.rs @@ -24,7 +24,7 @@ use crate::{ use frame_support::{ pallet_prelude::*, traits::{ - fungibles::{Balanced, CreditOf}, + fungibles::{Balanced, Credit}, Currency, OnUnbalanced, }, }; @@ -45,7 +45,7 @@ impl OnUnbalanced for Author { /// Will drop and burn the assets in case the transfer fails. pub struct CreditToBlockAuthor; impl HandleCredit for CreditToBlockAuthor { - fn handle_credit(credit: CreditOf) { + fn handle_credit(credit: Credit) { if let Some(author) = pallet_authorship::Pallet::::author() { // Drop the result which will trigger the `OnDrop` of the imbalance in case of error. let _ = Assets::resolve(&author, credit); diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 9f4ad2ed26..3f0ea07122 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -59,6 +59,7 @@ use pallet_nis::WithMaximumOf; use pallet_session::historical as pallet_session_historical; pub use pallet_transaction_payment::{CurrencyAdapter, Multiplier, TargetedFeeAdjustment}; use pallet_transaction_payment::{FeeDetails, RuntimeDispatchInfo}; +use scale_info::TypeInfo; use sp_api::impl_runtime_apis; use sp_authority_discovery::AuthorityId as AuthorityDiscoveryId; use sp_consensus_grandpa::AuthorityId as GrandpaId; @@ -432,6 +433,15 @@ parameter_types! { pub const MaxReserves: u32 = 50; } +/// A reason for placing a hold on funds. +#[derive( + Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, Debug, TypeInfo, +)] +pub enum HoldReason { + /// The NIS Pallet has reserved it for a non-fungible receipt. + Nis, +} + impl pallet_balances::Config for Runtime { type MaxLocks = MaxLocks; type MaxReserves = MaxReserves; @@ -442,6 +452,10 @@ impl pallet_balances::Config for Runtime { type ExistentialDeposit = ExistentialDeposit; type AccountStore = frame_system::Pallet; type WeightInfo = pallet_balances::weights::SubstrateWeight; + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = HoldReason; + type MaxHolds = ConstU32<1>; } parameter_types! { @@ -1481,7 +1495,6 @@ impl pallet_assets::Config for Runtime { } parameter_types! { - pub IgnoredIssuance: Balance = Treasury::pot(); pub const QueueCount: u32 = 300; pub const MaxQueueLen: u32 = 1000; pub const FifoQueueLen: u32 = 500; @@ -1493,7 +1506,7 @@ parameter_types! { pub const ThawThrottle: (Perquintill, BlockNumber) = (Perquintill::from_percent(25), 5); pub Target: Perquintill = Perquintill::zero(); pub const NisPalletId: PalletId = PalletId(*b"py/nis "); - pub const NisReserveId: [u8; 8] = *b"py/nis "; + pub const NisHoldReason: HoldReason = HoldReason::Nis; } impl pallet_nis::Config for Runtime { @@ -1505,7 +1518,7 @@ impl pallet_nis::Config for Runtime { type Counterpart = ItemOf, AccountId>; type CounterpartAmount = WithMaximumOf>; type Deficit = (); - type IgnoredIssuance = IgnoredIssuance; + type IgnoredIssuance = (); type Target = Target; type PalletId = NisPalletId; type QueueCount = QueueCount; @@ -1517,7 +1530,7 @@ impl pallet_nis::Config for Runtime { type IntakePeriod = IntakePeriod; type MaxIntakeWeight = MaxIntakeWeight; type ThawThrottle = ThawThrottle; - type ReserveId = NisReserveId; + type HoldReason = NisHoldReason; } parameter_types! { diff --git a/bin/node/testing/src/bench.rs b/bin/node/testing/src/bench.rs index 1a9af13028..d6bcf6e252 100644 --- a/bin/node/testing/src/bench.rs +++ b/bin/node/testing/src/bench.rs @@ -308,7 +308,7 @@ impl<'a> Iterator for BlockContentIterator<'a> { value: kitchensink_runtime::ExistentialDeposit::get() + 1, }), BlockType::RandomTransfersReaping => { - RuntimeCall::Balances(BalancesCall::transfer { + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: sp_runtime::MultiAddress::Id(receiver), // Transfer so that ending balance would be 1 less than existential // deposit so that we kill the sender account. diff --git a/frame/alliance/src/mock.rs b/frame/alliance/src/mock.rs index b8fd998eb2..848f6b2a00 100644 --- a/frame/alliance/src/mock.rs +++ b/frame/alliance/src/mock.rs @@ -85,6 +85,10 @@ impl pallet_balances::Config for Test { type MaxLocks = MaxLocks; type MaxReserves = (); type ReserveIdentifier = [u8; 8]; + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } const MOTION_DURATION_IN_BLOCKS: BlockNumber = 3; diff --git a/frame/assets/src/benchmarking.rs b/frame/assets/src/benchmarking.rs index 5b4f1489df..047ede2ce6 100644 --- a/frame/assets/src/benchmarking.rs +++ b/frame/assets/src/benchmarking.rs @@ -105,14 +105,18 @@ fn add_sufficients, I: 'static>(minter: T::AccountId, n: u32) { fn add_approvals, I: 'static>(minter: T::AccountId, n: u32) { let asset_id = default_asset_id::(); - T::Currency::deposit_creating(&minter, T::ApprovalDeposit::get() * n.into()); + T::Currency::deposit_creating( + &minter, + T::ApprovalDeposit::get() * n.into() + T::Currency::minimum_balance(), + ); let minter_lookup = T::Lookup::unlookup(minter.clone()); let origin = SystemOrigin::Signed(minter); Assets::::mint(origin.clone().into(), asset_id, minter_lookup, (100 * (n + 1)).into()) .unwrap(); + let enough = T::Currency::minimum_balance(); for i in 0..n { let target = account("approval", i, SEED); - T::Currency::make_free_balance_be(&target, T::Currency::minimum_balance()); + T::Currency::make_free_balance_be(&target, enough); let target_lookup = T::Lookup::unlookup(target); Assets::::approve_transfer( origin.clone().into(), diff --git a/frame/assets/src/functions.rs b/frame/assets/src/functions.rs index 47657ff267..af9e269acf 100644 --- a/frame/assets/src/functions.rs +++ b/frame/assets/src/functions.rs @@ -76,7 +76,14 @@ impl, I: 'static> Pallet { d.sufficients += 1; ExistenceReason::Sufficient } else { - frame_system::Pallet::::inc_consumers(who).map_err(|_| Error::::NoProvider)?; + frame_system::Pallet::::inc_consumers(who) + .map_err(|_| Error::::UnavailableConsumer)?; + // We ensure that we can still increment consumers once more because we could otherwise + // allow accidental usage of all consumer references which could cause grief. + if !frame_system::Pallet::::can_inc_consumer(who) { + frame_system::Pallet::::dec_consumers(who); + return Err(Error::::UnavailableConsumer.into()) + } ExistenceReason::Consumer }; d.accounts = accounts; @@ -165,7 +172,7 @@ impl, I: 'static> Pallet { } let account = match Account::::get(id, who) { Some(a) => a, - None => return NoFunds, + None => return BalanceLow, }; if account.is_frozen { return Frozen @@ -193,7 +200,7 @@ impl, I: 'static> Pallet { Success } } else { - NoFunds + BalanceLow } } @@ -254,7 +261,7 @@ impl, I: 'static> Pallet { ensure!(f.best_effort || actual >= amount, Error::::BalanceLow); let conseq = Self::can_decrease(id, target, actual, f.keep_alive); - let actual = match conseq.into_result() { + let actual = match conseq.into_result(f.keep_alive) { Ok(dust) => actual.saturating_add(dust), //< guaranteed by reducible_balance Err(e) => { debug_assert!(false, "passed from reducible_balance; qed"); diff --git a/frame/assets/src/impl_fungibles.rs b/frame/assets/src/impl_fungibles.rs index f420ea02c3..7bec884f4c 100644 --- a/frame/assets/src/impl_fungibles.rs +++ b/frame/assets/src/impl_fungibles.rs @@ -17,6 +17,16 @@ //! Implementations for fungibles trait. +use frame_support::{ + defensive, + traits::tokens::{ + Fortitude, + Precision::{self, BestEffort}, + Preservation::{self, Expendable}, + Provenance::{self, Minted}, + }, +}; + use super::*; impl, I: 'static> fungibles::Inspect<::AccountId> for Pallet { @@ -35,21 +45,27 @@ impl, I: 'static> fungibles::Inspect<::AccountId Pallet::::balance(asset, who) } + fn total_balance(asset: Self::AssetId, who: &::AccountId) -> Self::Balance { + Pallet::::balance(asset, who) + } + fn reducible_balance( asset: Self::AssetId, who: &::AccountId, - keep_alive: bool, + preservation: Preservation, + _: Fortitude, ) -> Self::Balance { - Pallet::::reducible_balance(asset, who, keep_alive).unwrap_or(Zero::zero()) + Pallet::::reducible_balance(asset, who, !matches!(preservation, Expendable)) + .unwrap_or(Zero::zero()) } fn can_deposit( asset: Self::AssetId, who: &::AccountId, amount: Self::Balance, - mint: bool, + provenance: Provenance, ) -> DepositConsequence { - Pallet::::can_increase(asset, who, amount, mint) + Pallet::::can_increase(asset, who, amount, provenance == Minted) } fn can_withdraw( @@ -65,69 +81,26 @@ impl, I: 'static> fungibles::Inspect<::AccountId } } -impl, I: 'static> fungibles::InspectMetadata<::AccountId> +impl, I: 'static> fungibles::Mutate<::AccountId> for Pallet {} +impl, I: 'static> fungibles::Balanced<::AccountId> for Pallet { - /// Return the name of an asset. - fn name(asset: &Self::AssetId) -> Vec { - Metadata::::get(asset).name.to_vec() - } - - /// Return the symbol of an asset. - fn symbol(asset: &Self::AssetId) -> Vec { - Metadata::::get(asset).symbol.to_vec() - } - - /// Return the decimals of an asset. - fn decimals(asset: &Self::AssetId) -> u8 { - Metadata::::get(asset).decimals - } -} - -impl, I: 'static> fungibles::Mutate<::AccountId> for Pallet { - fn mint_into( - asset: Self::AssetId, - who: &::AccountId, - amount: Self::Balance, - ) -> DispatchResult { - Self::do_mint(asset, who, amount, None) - } - - fn burn_from( - asset: Self::AssetId, - who: &::AccountId, - amount: Self::Balance, - ) -> Result { - let f = DebitFlags { keep_alive: false, best_effort: false }; - Self::do_burn(asset, who, amount, None, f) - } - - fn slash( - asset: Self::AssetId, - who: &::AccountId, - amount: Self::Balance, - ) -> Result { - let f = DebitFlags { keep_alive: false, best_effort: true }; - Self::do_burn(asset, who, amount, None, f) - } -} - -impl, I: 'static> fungibles::Transfer for Pallet { - fn transfer( - asset: Self::AssetId, - source: &T::AccountId, - dest: &T::AccountId, - amount: T::Balance, - keep_alive: bool, - ) -> Result { - let f = TransferFlags { keep_alive, best_effort: false, burn_dust: false }; - Self::do_transfer(asset, source, dest, amount, None, f) - } + type OnDropCredit = fungibles::DecreaseIssuance; + type OnDropDebt = fungibles::IncreaseIssuance; } impl, I: 'static> fungibles::Unbalanced for Pallet { - fn set_balance(_: Self::AssetId, _: &T::AccountId, _: Self::Balance) -> DispatchResult { - unreachable!("set_balance is not used if other functions are impl'd"); + fn handle_raw_dust(_: Self::AssetId, _: Self::Balance) {} + fn handle_dust(_: fungibles::Dust) { + defensive!("`decrease_balance` and `increase_balance` have non-default impls; nothing else calls this; qed"); + } + fn write_balance( + _: Self::AssetId, + _: &T::AccountId, + _: Self::Balance, + ) -> Result, DispatchError> { + defensive!("write_balance is not used if other functions are impl'd"); + Err(DispatchError::Unavailable) } fn set_total_issuance(id: T::AssetId, amount: Self::Balance) { Asset::::mutate_exists(id, |maybe_asset| { @@ -140,36 +113,27 @@ impl, I: 'static> fungibles::Unbalanced for Pallet Result { - let f = DebitFlags { keep_alive: false, best_effort: false }; + let f = DebitFlags { + keep_alive: preservation != Expendable, + best_effort: precision == BestEffort, + }; Self::decrease_balance(asset, who, amount, f, |_, _| Ok(())) } - fn decrease_balance_at_most( - asset: T::AssetId, - who: &T::AccountId, - amount: Self::Balance, - ) -> Self::Balance { - let f = DebitFlags { keep_alive: false, best_effort: true }; - Self::decrease_balance(asset, who, amount, f, |_, _| Ok(())).unwrap_or(Zero::zero()) - } fn increase_balance( asset: T::AssetId, who: &T::AccountId, amount: Self::Balance, + _: Precision, ) -> Result { Self::increase_balance(asset, who, amount, |_| Ok(()))?; Ok(amount) } - fn increase_balance_at_most( - asset: T::AssetId, - who: &T::AccountId, - amount: Self::Balance, - ) -> Self::Balance { - match Self::increase_balance(asset, who, amount, |_| Ok(())) { - Ok(()) => amount, - Err(_) => Zero::zero(), - } - } + + // TODO: #13196 implement deactivate/reactivate once we have inactive balance tracking. } impl, I: 'static> fungibles::Create for Pallet { diff --git a/frame/assets/src/lib.rs b/frame/assets/src/lib.rs index b36a5cabd3..859a13243d 100644 --- a/frame/assets/src/lib.rs +++ b/frame/assets/src/lib.rs @@ -540,9 +540,9 @@ pub mod pallet { /// Minimum balance should be non-zero. MinBalanceZero, /// Unable to increment the consumer reference counters on the account. Either no provider - /// reference exists to allow a non-zero balance of a non-self-sufficient asset, or the - /// maximum number of consumers has been reached. - NoProvider, + /// reference exists to allow a non-zero balance of a non-self-sufficient asset, or one + /// fewer then the maximum number of consumers has been reached. + UnavailableConsumer, /// Invalid metadata given. BadMetadata, /// No approval exists that would allow the transfer. diff --git a/frame/assets/src/mock.rs b/frame/assets/src/mock.rs index 3f456a7de3..3926d2fa8b 100644 --- a/frame/assets/src/mock.rs +++ b/frame/assets/src/mock.rs @@ -74,7 +74,7 @@ impl frame_system::Config for Test { type SystemWeightInfo = (); type SS58Prefix = (); type OnSetCode = (); - type MaxConsumers = ConstU32<2>; + type MaxConsumers = ConstU32<3>; } impl pallet_balances::Config for Test { @@ -87,6 +87,10 @@ impl pallet_balances::Config for Test { type MaxLocks = (); type MaxReserves = (); type ReserveIdentifier = [u8; 8]; + type HoldIdentifier = (); + type FreezeIdentifier = (); + type MaxHolds = (); + type MaxFreezes = (); } pub struct AssetsCallbackHandle; diff --git a/frame/assets/src/tests.rs b/frame/assets/src/tests.rs index bc61810a76..d4a49ac5a4 100644 --- a/frame/assets/src/tests.rs +++ b/frame/assets/src/tests.rs @@ -57,7 +57,10 @@ fn minting_too_many_insufficient_assets_fails() { Balances::make_free_balance_be(&1, 100); assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); - assert_noop!(Assets::mint(RuntimeOrigin::signed(1), 2, 1, 100), TokenError::CannotCreate); + assert_noop!( + Assets::mint(RuntimeOrigin::signed(1), 2, 1, 100), + Error::::UnavailableConsumer + ); Balances::make_free_balance_be(&2, 1); assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 100)); @@ -75,7 +78,10 @@ fn minting_insufficient_asset_with_deposit_should_work_when_consumers_exhausted( Balances::make_free_balance_be(&1, 100); assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); - assert_noop!(Assets::mint(RuntimeOrigin::signed(1), 2, 1, 100), TokenError::CannotCreate); + assert_noop!( + Assets::mint(RuntimeOrigin::signed(1), 2, 1, 100), + Error::::UnavailableConsumer + ); assert_ok!(Assets::touch(RuntimeOrigin::signed(1), 2)); assert_eq!(Balances::reserved_balance(&1), 10); @@ -93,7 +99,7 @@ fn minting_insufficient_assets_with_deposit_without_consumer_should_work() { assert_ok!(Assets::touch(RuntimeOrigin::signed(1), 0)); assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); assert_eq!(Balances::reserved_balance(&1), 10); - assert_eq!(System::consumers(&1), 0); + assert_eq!(System::consumers(&1), 1); }); } @@ -167,7 +173,7 @@ fn approval_lifecycle_works() { // so we create it :) assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&1, 1); + Balances::make_free_balance_be(&1, 2); assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); assert_eq!(Asset::::get(0).unwrap().approvals, 1); assert_eq!(Balances::reserved_balance(&1), 1); @@ -193,7 +199,7 @@ fn transfer_approved_all_funds() { // so we create it :) assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&1, 1); + Balances::make_free_balance_be(&1, 2); assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); assert_eq!(Asset::::get(0).unwrap().approvals, 1); assert_eq!(Balances::reserved_balance(&1), 1); @@ -215,7 +221,7 @@ fn approval_deposits_work() { let e = BalancesError::::InsufficientBalance; assert_noop!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50), e); - Balances::make_free_balance_be(&1, 1); + Balances::make_free_balance_be(&1, 2); assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); assert_eq!(Balances::reserved_balance(&1), 1); @@ -233,7 +239,7 @@ fn cannot_transfer_more_than_approved() { new_test_ext().execute_with(|| { assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&1, 1); + Balances::make_free_balance_be(&1, 2); assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); let e = Error::::Unapproved; assert_noop!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 3, 51), e); @@ -245,7 +251,7 @@ fn cannot_transfer_more_than_exists() { new_test_ext().execute_with(|| { assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&1, 1); + Balances::make_free_balance_be(&1, 2); assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 101)); let e = Error::::BalanceLow; assert_noop!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 3, 101), e); @@ -257,7 +263,7 @@ fn cancel_approval_works() { new_test_ext().execute_with(|| { assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&1, 1); + Balances::make_free_balance_be(&1, 2); assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); assert_eq!(Asset::::get(0).unwrap().approvals, 1); assert_noop!( @@ -287,7 +293,7 @@ fn force_cancel_approval_works() { new_test_ext().execute_with(|| { assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&1, 1); + Balances::make_free_balance_be(&1, 2); assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 50)); assert_eq!(Asset::::get(0).unwrap().approvals, 1); let e = Error::::NoPermission; @@ -516,7 +522,7 @@ fn min_balance_should_work() { // Death by `transfer_approved`. assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&1, 1); + Balances::make_free_balance_be(&1, 2); assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 100)); assert_ok!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 3, 91)); assert_eq!(take_hooks(), vec![Hook::Died(0, 1)]); @@ -1217,7 +1223,7 @@ fn querying_allowance_should_work() { use frame_support::traits::tokens::fungibles::approvals::{Inspect, Mutate}; assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); - Balances::make_free_balance_be(&1, 1); + Balances::make_free_balance_be(&1, 2); assert_ok!(Assets::approve(0, &1, &2, 50)); assert_eq!(Assets::allowance(0, &1, &2), 50); // Transfer asset 0, from owner 1 and delegate 2 to destination 3 diff --git a/frame/atomic-swap/src/tests.rs b/frame/atomic-swap/src/tests.rs index 081a449ec9..7437d62a99 100644 --- a/frame/atomic-swap/src/tests.rs +++ b/frame/atomic-swap/src/tests.rs @@ -62,6 +62,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } impl Config for Test { diff --git a/frame/babe/src/mock.rs b/frame/babe/src/mock.rs index 9b832bfffd..94e748d0bc 100644 --- a/frame/babe/src/mock.rs +++ b/frame/babe/src/mock.rs @@ -144,6 +144,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU128<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } pallet_staking_reward_curve::build! { diff --git a/frame/balances/README.md b/frame/balances/README.md index 93e424a89c..dd56ab3fad 100644 --- a/frame/balances/README.md +++ b/frame/balances/README.md @@ -70,7 +70,7 @@ given account is unused. ### Dispatchable Functions - `transfer` - Transfer some liquid free balance to another account. -- `set_balance` - Set the balances of a given account. The origin of this call must be root. +- `force_set_balance` - Set the balances of a given account. The origin of this call must be root. ## Usage diff --git a/frame/balances/src/benchmarking.rs b/frame/balances/src/benchmarking.rs index b36fe1e341..5641c68516 100644 --- a/frame/balances/src/benchmarking.rs +++ b/frame/balances/src/benchmarking.rs @@ -24,6 +24,8 @@ use crate::Pallet as Balances; use frame_benchmarking::v2::*; use frame_system::RawOrigin; +use sp_runtime::traits::Bounded; +use types::ExtraFlags; const SEED: u32 = 0; // existential deposit multiplier @@ -37,7 +39,7 @@ mod benchmarks { // * Transfer will kill the sender account. // * Transfer will create the recipient account. #[benchmark] - fn transfer() { + fn transfer_allow_death() { let existential_deposit = T::ExistentialDeposit::get(); let caller = whitelisted_caller(); @@ -79,7 +81,7 @@ mod benchmarks { let transfer_amount = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); #[extrinsic_call] - transfer(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount); + transfer_allow_death(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount); assert!(!Balances::::free_balance(&caller).is_zero()); assert!(!Balances::::free_balance(&recipient).is_zero()); @@ -106,9 +108,9 @@ mod benchmarks { assert_eq!(Balances::::free_balance(&recipient), transfer_amount); } - // Benchmark `set_balance` coming from ROOT account. This always creates an account. + // Benchmark `force_set_balance` coming from ROOT account. This always creates an account. #[benchmark] - fn set_balance_creating() { + fn force_set_balance_creating() { let user: T::AccountId = account("user", 0, SEED); let user_lookup = T::Lookup::unlookup(user.clone()); @@ -118,15 +120,14 @@ mod benchmarks { let _ = as Currency<_>>::make_free_balance_be(&user, balance_amount); #[extrinsic_call] - set_balance(RawOrigin::Root, user_lookup, balance_amount, balance_amount); + force_set_balance(RawOrigin::Root, user_lookup, balance_amount); assert_eq!(Balances::::free_balance(&user), balance_amount); - assert_eq!(Balances::::reserved_balance(&user), balance_amount); } - // Benchmark `set_balance` coming from ROOT account. This always kills an account. + // Benchmark `force_set_balance` coming from ROOT account. This always kills an account. #[benchmark] - fn set_balance_killing() { + fn force_set_balance_killing() { let user: T::AccountId = account("user", 0, SEED); let user_lookup = T::Lookup::unlookup(user.clone()); @@ -136,7 +137,7 @@ mod benchmarks { let _ = as Currency<_>>::make_free_balance_be(&user, balance_amount); #[extrinsic_call] - set_balance(RawOrigin::Root, user_lookup, Zero::zero(), Zero::zero()); + force_set_balance(RawOrigin::Root, user_lookup, Zero::zero()); assert!(Balances::::free_balance(&user).is_zero()); } @@ -197,7 +198,7 @@ mod benchmarks { } #[extrinsic_call] - transfer(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount); + transfer_allow_death(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount); assert_eq!(Balances::::free_balance(&caller), Zero::zero()); assert_eq!(Balances::::free_balance(&recipient), transfer_amount); @@ -230,27 +231,64 @@ mod benchmarks { let user_lookup = T::Lookup::unlookup(user.clone()); // Give some multiple of the existential deposit - let existential_deposit = T::ExistentialDeposit::get(); - let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); + let ed = T::ExistentialDeposit::get(); + let balance = ed + ed; let _ = as Currency<_>>::make_free_balance_be(&user, balance); // Reserve the balance - as ReservableCurrency<_>>::reserve(&user, balance)?; - assert_eq!(Balances::::reserved_balance(&user), balance); - assert!(Balances::::free_balance(&user).is_zero()); + as ReservableCurrency<_>>::reserve(&user, ed)?; + assert_eq!(Balances::::reserved_balance(&user), ed); + assert_eq!(Balances::::free_balance(&user), ed); #[extrinsic_call] _(RawOrigin::Root, user_lookup, balance); assert!(Balances::::reserved_balance(&user).is_zero()); - assert_eq!(Balances::::free_balance(&user), balance); + assert_eq!(Balances::::free_balance(&user), ed + ed); Ok(()) } + #[benchmark] + fn upgrade_accounts(u: Linear<1, 1_000>) { + let caller: T::AccountId = whitelisted_caller(); + let who = (0..u) + .into_iter() + .map(|i| -> T::AccountId { + let user = account("old_user", i, SEED); + let account = AccountData { + free: T::ExistentialDeposit::get(), + reserved: T::ExistentialDeposit::get(), + frozen: Zero::zero(), + flags: ExtraFlags::old_logic(), + }; + frame_system::Pallet::::inc_providers(&user); + assert!(T::AccountStore::try_mutate_exists(&user, |a| -> DispatchResult { + *a = Some(account); + Ok(()) + }) + .is_ok()); + assert!(!Balances::::account(&user).flags.is_new_logic()); + assert_eq!(frame_system::Pallet::::providers(&user), 1); + assert_eq!(frame_system::Pallet::::consumers(&user), 0); + user + }) + .collect(); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), who); + + for i in 0..u { + let user: T::AccountId = account("old_user", i, SEED); + assert!(Balances::::account(&user).flags.is_new_logic()); + assert_eq!(frame_system::Pallet::::providers(&user), 1); + assert_eq!(frame_system::Pallet::::consumers(&user), 1); + } + } + impl_benchmark_test_suite! { Balances, - crate::tests_composite::ExtBuilder::default().build(), - crate::tests_composite::Test, + crate::tests::ExtBuilder::default().build(), + crate::tests::Test, } } diff --git a/frame/balances/src/impl_currency.rs b/frame/balances/src/impl_currency.rs new file mode 100644 index 0000000000..790a29f004 --- /dev/null +++ b/frame/balances/src/impl_currency.rs @@ -0,0 +1,898 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Implementations for the `Currency` family of traits. + +use super::*; +use frame_support::{ + ensure, + pallet_prelude::DispatchResult, + traits::{ + tokens::{fungible, BalanceStatus as Status}, + Currency, DefensiveSaturating, ExistenceRequirement, + ExistenceRequirement::AllowDeath, + Get, Imbalance, LockIdentifier, LockableCurrency, NamedReservableCurrency, + ReservableCurrency, SignedImbalance, TryDrop, WithdrawReasons, + }, +}; +pub use imbalances::{NegativeImbalance, PositiveImbalance}; + +// wrapping these imbalances in a private module is necessary to ensure absolute privacy +// of the inner member. +mod imbalances { + use super::{result, Config, Imbalance, RuntimeDebug, Saturating, TryDrop, Zero}; + use frame_support::traits::SameOrOther; + use sp_std::mem; + + /// Opaque, move-only struct with private fields that serves as a token denoting that + /// funds have been created without any equal and opposite accounting. + #[must_use] + #[derive(RuntimeDebug, PartialEq, Eq)] + pub struct PositiveImbalance, I: 'static = ()>(T::Balance); + + impl, I: 'static> PositiveImbalance { + /// Create a new positive imbalance from a balance. + pub fn new(amount: T::Balance) -> Self { + PositiveImbalance(amount) + } + } + + /// Opaque, move-only struct with private fields that serves as a token denoting that + /// funds have been destroyed without any equal and opposite accounting. + #[must_use] + #[derive(RuntimeDebug, PartialEq, Eq)] + pub struct NegativeImbalance, I: 'static = ()>(T::Balance); + + impl, I: 'static> NegativeImbalance { + /// Create a new negative imbalance from a balance. + pub fn new(amount: T::Balance) -> Self { + NegativeImbalance(amount) + } + } + + impl, I: 'static> TryDrop for PositiveImbalance { + fn try_drop(self) -> result::Result<(), Self> { + self.drop_zero() + } + } + + impl, I: 'static> Default for PositiveImbalance { + fn default() -> Self { + Self::zero() + } + } + + impl, I: 'static> Imbalance for PositiveImbalance { + type Opposite = NegativeImbalance; + + fn zero() -> Self { + Self(Zero::zero()) + } + fn drop_zero(self) -> result::Result<(), Self> { + if self.0.is_zero() { + Ok(()) + } else { + Err(self) + } + } + fn split(self, amount: T::Balance) -> (Self, Self) { + let first = self.0.min(amount); + let second = self.0 - first; + + mem::forget(self); + (Self(first), Self(second)) + } + fn merge(mut self, other: Self) -> Self { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + + self + } + fn subsume(&mut self, other: Self) { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + } + fn offset(self, other: Self::Opposite) -> SameOrOther { + let (a, b) = (self.0, other.0); + mem::forget((self, other)); + + if a > b { + SameOrOther::Same(Self(a - b)) + } else if b > a { + SameOrOther::Other(NegativeImbalance::new(b - a)) + } else { + SameOrOther::None + } + } + fn peek(&self) -> T::Balance { + self.0 + } + } + + impl, I: 'static> TryDrop for NegativeImbalance { + fn try_drop(self) -> result::Result<(), Self> { + self.drop_zero() + } + } + + impl, I: 'static> Default for NegativeImbalance { + fn default() -> Self { + Self::zero() + } + } + + impl, I: 'static> Imbalance for NegativeImbalance { + type Opposite = PositiveImbalance; + + fn zero() -> Self { + Self(Zero::zero()) + } + fn drop_zero(self) -> result::Result<(), Self> { + if self.0.is_zero() { + Ok(()) + } else { + Err(self) + } + } + fn split(self, amount: T::Balance) -> (Self, Self) { + let first = self.0.min(amount); + let second = self.0 - first; + + mem::forget(self); + (Self(first), Self(second)) + } + fn merge(mut self, other: Self) -> Self { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + + self + } + fn subsume(&mut self, other: Self) { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + } + fn offset(self, other: Self::Opposite) -> SameOrOther { + let (a, b) = (self.0, other.0); + mem::forget((self, other)); + + if a > b { + SameOrOther::Same(Self(a - b)) + } else if b > a { + SameOrOther::Other(PositiveImbalance::new(b - a)) + } else { + SameOrOther::None + } + } + fn peek(&self) -> T::Balance { + self.0 + } + } + + impl, I: 'static> Drop for PositiveImbalance { + /// Basic drop handler will just square up the total issuance. + fn drop(&mut self) { + >::mutate(|v| *v = v.saturating_add(self.0)); + } + } + + impl, I: 'static> Drop for NegativeImbalance { + /// Basic drop handler will just square up the total issuance. + fn drop(&mut self) { + >::mutate(|v| *v = v.saturating_sub(self.0)); + } + } +} + +impl, I: 'static> Currency for Pallet +where + T::Balance: MaybeSerializeDeserialize + Debug, +{ + type Balance = T::Balance; + type PositiveImbalance = PositiveImbalance; + type NegativeImbalance = NegativeImbalance; + + fn total_balance(who: &T::AccountId) -> Self::Balance { + Self::account(who).total() + } + + // Check if `value` amount of free balance can be slashed from `who`. + fn can_slash(who: &T::AccountId, value: Self::Balance) -> bool { + if value.is_zero() { + return true + } + Self::free_balance(who) >= value + } + + fn total_issuance() -> Self::Balance { + TotalIssuance::::get() + } + + fn active_issuance() -> Self::Balance { + >::active_issuance() + } + + fn deactivate(amount: Self::Balance) { + >::deactivate(amount); + } + + fn reactivate(amount: Self::Balance) { + >::reactivate(amount); + } + + fn minimum_balance() -> Self::Balance { + T::ExistentialDeposit::get() + } + + // Burn funds from the total issuance, returning a positive imbalance for the amount burned. + // Is a no-op if amount to be burned is zero. + fn burn(mut amount: Self::Balance) -> Self::PositiveImbalance { + if amount.is_zero() { + return PositiveImbalance::zero() + } + >::mutate(|issued| { + *issued = issued.checked_sub(&amount).unwrap_or_else(|| { + amount = *issued; + Zero::zero() + }); + }); + PositiveImbalance::new(amount) + } + + // Create new funds into the total issuance, returning a negative imbalance + // for the amount issued. + // Is a no-op if amount to be issued it zero. + fn issue(mut amount: Self::Balance) -> Self::NegativeImbalance { + if amount.is_zero() { + return NegativeImbalance::zero() + } + >::mutate(|issued| { + *issued = issued.checked_add(&amount).unwrap_or_else(|| { + amount = Self::Balance::max_value() - *issued; + Self::Balance::max_value() + }) + }); + NegativeImbalance::new(amount) + } + + fn free_balance(who: &T::AccountId) -> Self::Balance { + Self::account(who).free + } + + // Ensure that an account can withdraw from their free balance given any existing withdrawal + // restrictions like locks and vesting balance. + // Is a no-op if amount to be withdrawn is zero. + fn ensure_can_withdraw( + who: &T::AccountId, + amount: T::Balance, + _reasons: WithdrawReasons, + new_balance: T::Balance, + ) -> DispatchResult { + if amount.is_zero() { + return Ok(()) + } + ensure!(new_balance >= Self::account(who).frozen, Error::::LiquidityRestrictions); + Ok(()) + } + + // Transfer some free balance from `transactor` to `dest`, respecting existence requirements. + // Is a no-op if value to be transferred is zero or the `transactor` is the same as `dest`. + fn transfer( + transactor: &T::AccountId, + dest: &T::AccountId, + value: Self::Balance, + existence_requirement: ExistenceRequirement, + ) -> DispatchResult { + if value.is_zero() || transactor == dest { + return Ok(()) + } + let keep_alive = match existence_requirement { + ExistenceRequirement::KeepAlive => Preserve, + ExistenceRequirement::AllowDeath => Expendable, + }; + >::transfer(transactor, dest, value, keep_alive)?; + Ok(()) + } + + /// Slash a target account `who`, returning the negative imbalance created and any left over + /// amount that could not be slashed. + /// + /// Is a no-op if `value` to be slashed is zero or the account does not exist. + /// + /// NOTE: `slash()` prefers free balance, but assumes that reserve balance can be drawn + /// from in extreme circumstances. `can_slash()` should be used prior to `slash()` to avoid + /// having to draw from reserved funds, however we err on the side of punishment if things are + /// inconsistent or `can_slash` wasn't used appropriately. + fn slash(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { + if value.is_zero() { + return (NegativeImbalance::zero(), Zero::zero()) + } + if Self::total_balance(who).is_zero() { + return (NegativeImbalance::zero(), value) + } + + let result = match Self::try_mutate_account_handling_dust( + who, + |account, _is_new| -> Result<(Self::NegativeImbalance, Self::Balance), DispatchError> { + // Best value is the most amount we can slash following liveness rules. + let ed = T::ExistentialDeposit::get(); + let actual = match system::Pallet::::can_dec_provider(who) { + true => value.min(account.free), + false => value.min(account.free.saturating_sub(ed)), + }; + account.free.saturating_reduce(actual); + let remaining = value.saturating_sub(actual); + Ok((NegativeImbalance::new(actual), remaining)) + }, + ) { + Ok((imbalance, remaining)) => { + Self::deposit_event(Event::Slashed { + who: who.clone(), + amount: value.saturating_sub(remaining), + }); + (imbalance, remaining) + }, + Err(_) => (Self::NegativeImbalance::zero(), value), + }; + result + } + + /// Deposit some `value` into the free balance of an existing target account `who`. + /// + /// Is a no-op if the `value` to be deposited is zero. + fn deposit_into_existing( + who: &T::AccountId, + value: Self::Balance, + ) -> Result { + if value.is_zero() { + return Ok(PositiveImbalance::zero()) + } + + Self::try_mutate_account_handling_dust( + who, + |account, is_new| -> Result { + ensure!(!is_new, Error::::DeadAccount); + account.free = account.free.checked_add(&value).ok_or(ArithmeticError::Overflow)?; + Self::deposit_event(Event::Deposit { who: who.clone(), amount: value }); + Ok(PositiveImbalance::new(value)) + }, + ) + } + + /// Deposit some `value` into the free balance of `who`, possibly creating a new account. + /// + /// This function is a no-op if: + /// - the `value` to be deposited is zero; or + /// - the `value` to be deposited is less than the required ED and the account does not yet + /// exist; or + /// - the deposit would necessitate the account to exist and there are no provider references; + /// or + /// - `value` is so large it would cause the balance of `who` to overflow. + fn deposit_creating(who: &T::AccountId, value: Self::Balance) -> Self::PositiveImbalance { + if value.is_zero() { + return Self::PositiveImbalance::zero() + } + + Self::try_mutate_account_handling_dust( + who, + |account, is_new| -> Result { + let ed = T::ExistentialDeposit::get(); + ensure!(value >= ed || !is_new, Error::::ExistentialDeposit); + + // defensive only: overflow should never happen, however in case it does, then this + // operation is a no-op. + account.free = match account.free.checked_add(&value) { + Some(x) => x, + None => return Ok(Self::PositiveImbalance::zero()), + }; + + Self::deposit_event(Event::Deposit { who: who.clone(), amount: value }); + Ok(PositiveImbalance::new(value)) + }, + ) + .unwrap_or_else(|_| Self::PositiveImbalance::zero()) + } + + /// Withdraw some free balance from an account, respecting existence requirements. + /// + /// Is a no-op if value to be withdrawn is zero. + fn withdraw( + who: &T::AccountId, + value: Self::Balance, + reasons: WithdrawReasons, + liveness: ExistenceRequirement, + ) -> result::Result { + if value.is_zero() { + return Ok(NegativeImbalance::zero()) + } + + Self::try_mutate_account_handling_dust( + who, + |account, _| -> Result { + let new_free_account = + account.free.checked_sub(&value).ok_or(Error::::InsufficientBalance)?; + + // bail if we need to keep the account alive and this would kill it. + let ed = T::ExistentialDeposit::get(); + let would_be_dead = new_free_account < ed; + let would_kill = would_be_dead && account.free >= ed; + ensure!(liveness == AllowDeath || !would_kill, Error::::Expendability); + + Self::ensure_can_withdraw(who, value, reasons, new_free_account)?; + + account.free = new_free_account; + + Self::deposit_event(Event::Withdraw { who: who.clone(), amount: value }); + Ok(NegativeImbalance::new(value)) + }, + ) + } + + /// Force the new free balance of a target account `who` to some new value `balance`. + fn make_free_balance_be( + who: &T::AccountId, + value: Self::Balance, + ) -> SignedImbalance { + Self::try_mutate_account_handling_dust( + who, + |account, + is_new| + -> Result, DispatchError> { + let ed = T::ExistentialDeposit::get(); + // If we're attempting to set an existing account to less than ED, then + // bypass the entire operation. It's a no-op if you follow it through, but + // since this is an instance where we might account for a negative imbalance + // (in the dust cleaner of set_account) before we account for its actual + // equal and opposite cause (returned as an Imbalance), then in the + // instance that there's no other accounts on the system at all, we might + // underflow the issuance and our arithmetic will be off. + ensure!(value >= ed || !is_new, Error::::ExistentialDeposit); + + let imbalance = if account.free <= value { + SignedImbalance::Positive(PositiveImbalance::new(value - account.free)) + } else { + SignedImbalance::Negative(NegativeImbalance::new(account.free - value)) + }; + account.free = value; + Self::deposit_event(Event::BalanceSet { who: who.clone(), free: account.free }); + Ok(imbalance) + }, + ) + .unwrap_or_else(|_| SignedImbalance::Positive(Self::PositiveImbalance::zero())) + } +} + +impl, I: 'static> ReservableCurrency for Pallet +where + T::Balance: MaybeSerializeDeserialize + Debug, +{ + /// Check if `who` can reserve `value` from their free balance. + /// + /// Always `true` if value to be reserved is zero. + fn can_reserve(who: &T::AccountId, value: Self::Balance) -> bool { + if value.is_zero() { + return true + } + Self::account(who).free.checked_sub(&value).map_or(false, |new_balance| { + Self::ensure_can_withdraw(who, value, WithdrawReasons::RESERVE, new_balance).is_ok() + }) + } + + fn reserved_balance(who: &T::AccountId) -> Self::Balance { + Self::account(who).reserved + } + + /// Move `value` from the free balance from `who` to their reserved balance. + /// + /// Is a no-op if value to be reserved is zero. + fn reserve(who: &T::AccountId, value: Self::Balance) -> DispatchResult { + if value.is_zero() { + return Ok(()) + } + + Self::try_mutate_account_handling_dust(who, |account, _| -> DispatchResult { + account.free = + account.free.checked_sub(&value).ok_or(Error::::InsufficientBalance)?; + account.reserved = + account.reserved.checked_add(&value).ok_or(ArithmeticError::Overflow)?; + Self::ensure_can_withdraw(&who, value, WithdrawReasons::RESERVE, account.free) + })?; + + Self::deposit_event(Event::Reserved { who: who.clone(), amount: value }); + Ok(()) + } + + /// Unreserve some funds, returning any amount that was unable to be unreserved. + /// + /// Is a no-op if the value to be unreserved is zero or the account does not exist. + /// + /// NOTE: returns amount value which wasn't successfully unreserved. + fn unreserve(who: &T::AccountId, value: Self::Balance) -> Self::Balance { + if value.is_zero() { + return Zero::zero() + } + if Self::total_balance(who).is_zero() { + return value + } + + let actual = match Self::mutate_account_handling_dust(who, |account| { + let actual = cmp::min(account.reserved, value); + account.reserved -= actual; + // defensive only: this can never fail since total issuance which is at least + // free+reserved fits into the same data type. + account.free = account.free.defensive_saturating_add(actual); + actual + }) { + Ok(x) => x, + Err(_) => { + // This should never happen since we don't alter the total amount in the account. + // If it ever does, then we should fail gracefully though, indicating that nothing + // could be done. + return value + }, + }; + + Self::deposit_event(Event::Unreserved { who: who.clone(), amount: actual }); + value - actual + } + + /// Slash from reserved balance, returning the negative imbalance created, + /// and any amount that was unable to be slashed. + /// + /// Is a no-op if the value to be slashed is zero or the account does not exist. + fn slash_reserved( + who: &T::AccountId, + value: Self::Balance, + ) -> (Self::NegativeImbalance, Self::Balance) { + if value.is_zero() { + return (NegativeImbalance::zero(), Zero::zero()) + } + if Self::total_balance(who).is_zero() { + return (NegativeImbalance::zero(), value) + } + + // NOTE: `mutate_account` may fail if it attempts to reduce the balance to the point that an + // account is attempted to be illegally destroyed. + + match Self::mutate_account_handling_dust(who, |account| { + let actual = value.min(account.reserved); + account.reserved.saturating_reduce(actual); + + // underflow should never happen, but it if does, there's nothing to be done here. + (NegativeImbalance::new(actual), value.saturating_sub(actual)) + }) { + Ok((imbalance, not_slashed)) => { + Self::deposit_event(Event::Slashed { + who: who.clone(), + amount: value.saturating_sub(not_slashed), + }); + (imbalance, not_slashed) + }, + Err(_) => (Self::NegativeImbalance::zero(), value), + } + } + + /// Move the reserved balance of one account into the balance of another, according to `status`. + /// + /// Is a no-op if: + /// - the value to be moved is zero; or + /// - the `slashed` id equal to `beneficiary` and the `status` is `Reserved`. + fn repatriate_reserved( + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: Status, + ) -> Result { + let actual = Self::do_transfer_reserved(slashed, beneficiary, value, true, status)?; + Ok(value.saturating_sub(actual)) + } +} + +impl, I: 'static> NamedReservableCurrency for Pallet +where + T::Balance: MaybeSerializeDeserialize + Debug, +{ + type ReserveIdentifier = T::ReserveIdentifier; + + fn reserved_balance_named(id: &Self::ReserveIdentifier, who: &T::AccountId) -> Self::Balance { + let reserves = Self::reserves(who); + reserves + .binary_search_by_key(id, |data| data.id) + .map(|index| reserves[index].amount) + .unwrap_or_default() + } + + /// Move `value` from the free balance from `who` to a named reserve balance. + /// + /// Is a no-op if value to be reserved is zero. + fn reserve_named( + id: &Self::ReserveIdentifier, + who: &T::AccountId, + value: Self::Balance, + ) -> DispatchResult { + if value.is_zero() { + return Ok(()) + } + + Reserves::::try_mutate(who, |reserves| -> DispatchResult { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + // this add can't overflow but just to be defensive. + reserves[index].amount = reserves[index].amount.defensive_saturating_add(value); + }, + Err(index) => { + reserves + .try_insert(index, ReserveData { id: *id, amount: value }) + .map_err(|_| Error::::TooManyReserves)?; + }, + }; + >::reserve(who, value)?; + Ok(()) + }) + } + + /// Unreserve some funds, returning any amount that was unable to be unreserved. + /// + /// Is a no-op if the value to be unreserved is zero. + fn unreserve_named( + id: &Self::ReserveIdentifier, + who: &T::AccountId, + value: Self::Balance, + ) -> Self::Balance { + if value.is_zero() { + return Zero::zero() + } + + Reserves::::mutate_exists(who, |maybe_reserves| -> Self::Balance { + if let Some(reserves) = maybe_reserves.as_mut() { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + let to_change = cmp::min(reserves[index].amount, value); + + let remain = >::unreserve(who, to_change); + + // remain should always be zero but just to be defensive here. + let actual = to_change.defensive_saturating_sub(remain); + + // `actual <= to_change` and `to_change <= amount`; qed; + reserves[index].amount -= actual; + + if reserves[index].amount.is_zero() { + if reserves.len() == 1 { + // no more named reserves + *maybe_reserves = None; + } else { + // remove this named reserve + reserves.remove(index); + } + } + + value - actual + }, + Err(_) => value, + } + } else { + value + } + }) + } + + /// Slash from reserved balance, returning the negative imbalance created, + /// and any amount that was unable to be slashed. + /// + /// Is a no-op if the value to be slashed is zero. + fn slash_reserved_named( + id: &Self::ReserveIdentifier, + who: &T::AccountId, + value: Self::Balance, + ) -> (Self::NegativeImbalance, Self::Balance) { + if value.is_zero() { + return (NegativeImbalance::zero(), Zero::zero()) + } + + Reserves::::mutate(who, |reserves| -> (Self::NegativeImbalance, Self::Balance) { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + let to_change = cmp::min(reserves[index].amount, value); + + let (imb, remain) = + >::slash_reserved(who, to_change); + + // remain should always be zero but just to be defensive here. + let actual = to_change.defensive_saturating_sub(remain); + + // `actual <= to_change` and `to_change <= amount`; qed; + reserves[index].amount -= actual; + + Self::deposit_event(Event::Slashed { who: who.clone(), amount: actual }); + (imb, value - actual) + }, + Err(_) => (NegativeImbalance::zero(), value), + } + }) + } + + /// Move the reserved balance of one account into the balance of another, according to `status`. + /// If `status` is `Reserved`, the balance will be reserved with given `id`. + /// + /// Is a no-op if: + /// - the value to be moved is zero; or + /// - the `slashed` id equal to `beneficiary` and the `status` is `Reserved`. + fn repatriate_reserved_named( + id: &Self::ReserveIdentifier, + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: Status, + ) -> Result { + if value.is_zero() { + return Ok(Zero::zero()) + } + + if slashed == beneficiary { + return match status { + Status::Free => Ok(Self::unreserve_named(id, slashed, value)), + Status::Reserved => + Ok(value.saturating_sub(Self::reserved_balance_named(id, slashed))), + } + } + + Reserves::::try_mutate(slashed, |reserves| -> Result { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + let to_change = cmp::min(reserves[index].amount, value); + + let actual = if status == Status::Reserved { + // make it the reserved under same identifier + Reserves::::try_mutate( + beneficiary, + |reserves| -> Result { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + let remain = + >::repatriate_reserved( + slashed, + beneficiary, + to_change, + status, + )?; + + // remain should always be zero but just to be defensive + // here. + let actual = to_change.defensive_saturating_sub(remain); + + // this add can't overflow but just to be defensive. + reserves[index].amount = + reserves[index].amount.defensive_saturating_add(actual); + + Ok(actual) + }, + Err(index) => { + let remain = + >::repatriate_reserved( + slashed, + beneficiary, + to_change, + status, + )?; + + // remain should always be zero but just to be defensive + // here + let actual = to_change.defensive_saturating_sub(remain); + + reserves + .try_insert( + index, + ReserveData { id: *id, amount: actual }, + ) + .map_err(|_| Error::::TooManyReserves)?; + + Ok(actual) + }, + } + }, + )? + } else { + let remain = >::repatriate_reserved( + slashed, + beneficiary, + to_change, + status, + )?; + + // remain should always be zero but just to be defensive here + to_change.defensive_saturating_sub(remain) + }; + + // `actual <= to_change` and `to_change <= amount`; qed; + reserves[index].amount -= actual; + + Ok(value - actual) + }, + Err(_) => Ok(value), + } + }) + } +} + +impl, I: 'static> LockableCurrency for Pallet +where + T::Balance: MaybeSerializeDeserialize + Debug, +{ + type Moment = T::BlockNumber; + + type MaxLocks = T::MaxLocks; + + // Set a lock on the balance of `who`. + // Is a no-op if lock amount is zero or `reasons` `is_none()`. + fn set_lock( + id: LockIdentifier, + who: &T::AccountId, + amount: T::Balance, + reasons: WithdrawReasons, + ) { + if amount.is_zero() || reasons.is_empty() { + return + } + let mut new_lock = Some(BalanceLock { id, amount, reasons: reasons.into() }); + let mut locks = Self::locks(who) + .into_iter() + .filter_map(|l| if l.id == id { new_lock.take() } else { Some(l) }) + .collect::>(); + if let Some(lock) = new_lock { + locks.push(lock) + } + Self::update_locks(who, &locks[..]); + } + + // Extend a lock on the balance of `who`. + // Is a no-op if lock amount is zero or `reasons` `is_none()`. + fn extend_lock( + id: LockIdentifier, + who: &T::AccountId, + amount: T::Balance, + reasons: WithdrawReasons, + ) { + if amount.is_zero() || reasons.is_empty() { + return + } + let mut new_lock = Some(BalanceLock { id, amount, reasons: reasons.into() }); + let mut locks = Self::locks(who) + .into_iter() + .filter_map(|l| { + if l.id == id { + new_lock.take().map(|nl| BalanceLock { + id: l.id, + amount: l.amount.max(nl.amount), + reasons: l.reasons | nl.reasons, + }) + } else { + Some(l) + } + }) + .collect::>(); + if let Some(lock) = new_lock { + locks.push(lock) + } + Self::update_locks(who, &locks[..]); + } + + fn remove_lock(id: LockIdentifier, who: &T::AccountId) { + let mut locks = Self::locks(who); + locks.retain(|l| l.id != id); + Self::update_locks(who, &locks[..]); + } +} diff --git a/frame/balances/src/impl_fungible.rs b/frame/balances/src/impl_fungible.rs new file mode 100644 index 0000000000..f8f8fe17ae --- /dev/null +++ b/frame/balances/src/impl_fungible.rs @@ -0,0 +1,358 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Implementation of `fungible` traits for Balances pallet. +use super::*; +use frame_support::traits::tokens::{ + Fortitude, + Preservation::{self, Preserve, Protect}, + Provenance::{self, Minted}, +}; + +impl, I: 'static> fungible::Inspect for Pallet { + type Balance = T::Balance; + + fn total_issuance() -> Self::Balance { + TotalIssuance::::get() + } + fn active_issuance() -> Self::Balance { + TotalIssuance::::get().saturating_sub(InactiveIssuance::::get()) + } + fn minimum_balance() -> Self::Balance { + T::ExistentialDeposit::get() + } + fn total_balance(who: &T::AccountId) -> Self::Balance { + Self::account(who).total() + } + fn balance(who: &T::AccountId) -> Self::Balance { + Self::account(who).free + } + fn reducible_balance( + who: &T::AccountId, + preservation: Preservation, + force: Fortitude, + ) -> Self::Balance { + let a = Self::account(who); + let mut untouchable = Zero::zero(); + if force == Polite { + // Frozen balance applies to total. Anything on hold therefore gets discounted from the + // limit given by the freezes. + untouchable = a.frozen.saturating_sub(a.reserved); + } + // If we want to keep our provider ref.. + if preservation == Preserve + // ..or we don't want the account to die and our provider ref is needed for it to live.. + || preservation == Protect && !a.free.is_zero() && + frame_system::Pallet::::providers(who) == 1 + // ..or we don't care about the account dying but our provider ref is required.. + || preservation == Expendable && !a.free.is_zero() && + !frame_system::Pallet::::can_dec_provider(who) + { + // ..then the ED needed.. + untouchable = untouchable.max(T::ExistentialDeposit::get()); + } + // Liquid balance is what is neither on hold nor frozen/required for provider. + a.free.saturating_sub(untouchable) + } + fn can_deposit( + who: &T::AccountId, + amount: Self::Balance, + provenance: Provenance, + ) -> DepositConsequence { + if amount.is_zero() { + return DepositConsequence::Success + } + + if provenance == Minted && TotalIssuance::::get().checked_add(&amount).is_none() { + return DepositConsequence::Overflow + } + + let account = Self::account(who); + let new_free = match account.free.checked_add(&amount) { + None => return DepositConsequence::Overflow, + Some(x) if x < T::ExistentialDeposit::get() => return DepositConsequence::BelowMinimum, + Some(x) => x, + }; + + match account.reserved.checked_add(&new_free) { + Some(_) => {}, + None => return DepositConsequence::Overflow, + }; + + // NOTE: We assume that we are a provider, so don't need to do any checks in the + // case of account creation. + + DepositConsequence::Success + } + fn can_withdraw( + who: &T::AccountId, + amount: Self::Balance, + ) -> WithdrawConsequence { + if amount.is_zero() { + return WithdrawConsequence::Success + } + + if TotalIssuance::::get().checked_sub(&amount).is_none() { + return WithdrawConsequence::Underflow + } + + let account = Self::account(who); + let new_free_balance = match account.free.checked_sub(&amount) { + Some(x) => x, + None => return WithdrawConsequence::BalanceLow, + }; + + let liquid = Self::reducible_balance(who, Expendable, Polite); + if amount > liquid { + return WithdrawConsequence::Frozen + } + + // Provider restriction - total account balance cannot be reduced to zero if it cannot + // sustain the loss of a provider reference. + // NOTE: This assumes that the pallet is a provider (which is true). Is this ever changes, + // then this will need to adapt accordingly. + let ed = T::ExistentialDeposit::get(); + let success = if new_free_balance < ed { + if frame_system::Pallet::::can_dec_provider(who) { + WithdrawConsequence::ReducedToZero(new_free_balance) + } else { + return WithdrawConsequence::WouldDie + } + } else { + WithdrawConsequence::Success + }; + + let new_total_balance = new_free_balance.saturating_add(account.reserved); + + // Eventual free funds must be no less than the frozen balance. + if new_total_balance < account.frozen { + return WithdrawConsequence::Frozen + } + + success + } +} + +impl, I: 'static> fungible::Unbalanced for Pallet { + fn handle_dust(dust: fungible::Dust) { + T::DustRemoval::on_unbalanced(dust.into_credit()); + } + fn write_balance( + who: &T::AccountId, + amount: Self::Balance, + ) -> Result, DispatchError> { + let max_reduction = + >::reducible_balance(who, Expendable, Force); + let (result, maybe_dust) = Self::mutate_account(who, |account| -> DispatchResult { + // Make sure the reduction (if there is one) is no more than the maximum allowed. + let reduction = account.free.saturating_sub(amount); + ensure!(reduction <= max_reduction, Error::::InsufficientBalance); + + account.free = amount; + Ok(()) + })?; + result?; + Ok(maybe_dust) + } + + fn set_total_issuance(amount: Self::Balance) { + TotalIssuance::::mutate(|t| *t = amount); + } + + fn deactivate(amount: Self::Balance) { + InactiveIssuance::::mutate(|b| b.saturating_accrue(amount)); + } + + fn reactivate(amount: Self::Balance) { + InactiveIssuance::::mutate(|b| b.saturating_reduce(amount)); + } +} + +impl, I: 'static> fungible::Mutate for Pallet { + fn done_mint_into(who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Minted { who: who.clone(), amount }); + } + fn done_burn_from(who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Burned { who: who.clone(), amount }); + } + fn done_shelve(who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Suspended { who: who.clone(), amount }); + } + fn done_restore(who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Restored { who: who.clone(), amount }); + } + fn done_transfer(source: &T::AccountId, dest: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Transfer { + from: source.clone(), + to: dest.clone(), + amount, + }); + } +} + +impl, I: 'static> fungible::MutateHold for Pallet {} + +impl, I: 'static> fungible::InspectHold for Pallet { + type Reason = T::HoldIdentifier; + + fn total_balance_on_hold(who: &T::AccountId) -> T::Balance { + Self::account(who).reserved + } + fn reducible_total_balance_on_hold(who: &T::AccountId, force: Fortitude) -> Self::Balance { + // The total balance must never drop below the freeze requirements if we're not forcing: + let a = Self::account(who); + let unavailable = if force == Force { + Self::Balance::zero() + } else { + // The freeze lock applies to the total balance, so we can discount the free balance + // from the amount which the total reserved balance must provide to satisfy it. + a.frozen.saturating_sub(a.free) + }; + a.reserved.saturating_sub(unavailable) + } + fn balance_on_hold(reason: &Self::Reason, who: &T::AccountId) -> T::Balance { + Holds::::get(who) + .iter() + .find(|x| &x.id == reason) + .map_or_else(Zero::zero, |x| x.amount) + } + fn hold_available(reason: &Self::Reason, who: &T::AccountId) -> bool { + if frame_system::Pallet::::providers(who) == 0 { + return false + } + let holds = Holds::::get(who); + if holds.is_full() && !holds.iter().any(|x| &x.id == reason) { + return false + } + true + } +} + +impl, I: 'static> fungible::UnbalancedHold for Pallet { + fn set_balance_on_hold( + reason: &Self::Reason, + who: &T::AccountId, + amount: Self::Balance, + ) -> DispatchResult { + let mut new_account = Self::account(who); + let mut holds = Holds::::get(who); + let mut increase = true; + let mut delta = amount; + + if let Some(item) = holds.iter_mut().find(|x| &x.id == reason) { + delta = item.amount.max(amount) - item.amount.min(amount); + increase = amount > item.amount; + item.amount = amount; + holds.retain(|x| !x.amount.is_zero()); + } else { + if !amount.is_zero() { + holds + .try_push(IdAmount { id: *reason, amount }) + .map_err(|_| Error::::TooManyHolds)?; + } + } + + new_account.reserved = if increase { + new_account.reserved.checked_add(&delta).ok_or(ArithmeticError::Overflow)? + } else { + new_account.reserved.checked_sub(&delta).ok_or(ArithmeticError::Underflow)? + }; + + let (result, maybe_dust) = Self::try_mutate_account(who, |a, _| -> DispatchResult { + *a = new_account; + Ok(()) + })?; + debug_assert!( + maybe_dust.is_none(), + "Does not alter main balance; dust only happens when it is altered; qed" + ); + Holds::::insert(who, holds); + Ok(result) + } +} + +impl, I: 'static> fungible::InspectFreeze for Pallet { + type Id = T::FreezeIdentifier; + + fn balance_frozen(id: &Self::Id, who: &T::AccountId) -> Self::Balance { + let locks = Freezes::::get(who); + locks.into_iter().find(|l| &l.id == id).map_or(Zero::zero(), |l| l.amount) + } + + fn can_freeze(id: &Self::Id, who: &T::AccountId) -> bool { + let l = Freezes::::get(who); + !l.is_full() || l.iter().any(|x| &x.id == id) + } +} + +impl, I: 'static> fungible::MutateFreeze for Pallet { + fn set_freeze(id: &Self::Id, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + if amount.is_zero() { + return Self::thaw(id, who) + } + let mut locks = Freezes::::get(who); + if let Some(i) = locks.iter_mut().find(|x| &x.id == id) { + i.amount = amount; + } else { + locks + .try_push(IdAmount { id: *id, amount }) + .map_err(|_| Error::::TooManyFreezes)?; + } + Self::update_freezes(who, locks.as_bounded_slice()) + } + + fn extend_freeze(id: &Self::Id, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + if amount.is_zero() { + return Ok(()) + } + let mut locks = Freezes::::get(who); + if let Some(i) = locks.iter_mut().find(|x| &x.id == id) { + i.amount = i.amount.max(amount); + } else { + locks + .try_push(IdAmount { id: *id, amount }) + .map_err(|_| Error::::TooManyFreezes)?; + } + Self::update_freezes(who, locks.as_bounded_slice()) + } + + fn thaw(id: &Self::Id, who: &T::AccountId) -> DispatchResult { + let mut locks = Freezes::::get(who); + locks.retain(|l| &l.id != id); + Self::update_freezes(who, locks.as_bounded_slice()) + } +} + +impl, I: 'static> fungible::Balanced for Pallet { + type OnDropCredit = fungible::DecreaseIssuance; + type OnDropDebt = fungible::IncreaseIssuance; + + fn done_deposit(who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Deposit { who: who.clone(), amount }); + } + fn done_withdraw(who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Withdraw { who: who.clone(), amount }); + } + fn done_issue(amount: Self::Balance) { + Self::deposit_event(Event::::Issued { amount }); + } + fn done_rescind(amount: Self::Balance) { + Self::deposit_event(Event::::Rescinded { amount }); + } +} + +impl, I: 'static> fungible::BalancedHold for Pallet {} diff --git a/frame/balances/src/lib.rs b/frame/balances/src/lib.rs index ecd7e171e3..ca8e86ef2f 100644 --- a/frame/balances/src/lib.rs +++ b/frame/balances/src/lib.rs @@ -89,8 +89,9 @@ //! //! ### Dispatchable Functions //! -//! - `transfer` - Transfer some liquid free balance to another account. -//! - `set_balance` - Set the balances of a given account. The origin of this call must be root. +//! - `transfer_allow_death` - Transfer some liquid free balance to another account. +//! - `force_set_balance` - Set the balances of a given account. The origin of this call must be +//! root. //! //! ## Usage //! @@ -152,43 +153,43 @@ //! * Total issued balanced of all accounts should be less than `Config::Balance::max_value()`. #![cfg_attr(not(feature = "std"), no_std)] - -#[macro_use] -mod tests; mod benchmarking; +mod impl_currency; +mod impl_fungible; pub mod migration; -mod tests_composite; -mod tests_local; -#[cfg(test)] -mod tests_reentrancy; +mod tests; +mod types; pub mod weights; -pub use self::imbalances::{NegativeImbalance, PositiveImbalance}; -use codec::{Codec, Decode, Encode, MaxEncodedLen}; +use codec::{Codec, MaxEncodedLen}; #[cfg(feature = "std")] use frame_support::traits::GenesisBuild; use frame_support::{ ensure, pallet_prelude::DispatchResult, traits::{ - tokens::{fungible, BalanceStatus as Status, DepositConsequence, WithdrawConsequence}, - Currency, DefensiveSaturating, ExistenceRequirement, - ExistenceRequirement::{AllowDeath, KeepAlive}, - Get, Imbalance, LockIdentifier, LockableCurrency, NamedReservableCurrency, OnUnbalanced, - ReservableCurrency, SignedImbalance, StoredMap, TryDrop, WithdrawReasons, + tokens::{ + fungible, BalanceStatus as Status, DepositConsequence, + Fortitude::{self, Force, Polite}, + Preservation::{Expendable, Preserve, Protect}, + WithdrawConsequence, + }, + Currency, Defensive, Get, OnUnbalanced, ReservableCurrency, StoredMap, }, - WeakBoundedVec, + BoundedSlice, WeakBoundedVec, }; use frame_system as system; +pub use impl_currency::{NegativeImbalance, PositiveImbalance}; use scale_info::TypeInfo; use sp_runtime::{ traits::{ AtLeast32BitUnsigned, Bounded, CheckedAdd, CheckedSub, MaybeSerializeDeserialize, Saturating, StaticLookup, Zero, }, - ArithmeticError, DispatchError, FixedPointOperand, RuntimeDebug, + ArithmeticError, DispatchError, FixedPointOperand, Perbill, RuntimeDebug, TokenError, }; -use sp_std::{cmp, fmt::Debug, mem, ops::BitOr, prelude::*, result}; +use sp_std::{cmp, fmt::Debug, mem, prelude::*, result}; +pub use types::{AccountData, BalanceLock, DustCleaner, IdAmount, Reasons, ReserveData}; pub use weights::WeightInfo; pub use pallet::*; @@ -200,11 +201,20 @@ type AccountIdLookupOf = <::Lookup as StaticLookup #[frame_support::pallet] pub mod pallet { use super::*; - use frame_support::pallet_prelude::*; + use frame_support::{pallet_prelude::*, traits::fungible::Credit}; use frame_system::pallet_prelude::*; + pub type CreditOf = Credit<::AccountId, Pallet>; + #[pallet::config] pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + /// The balance of an account. type Balance: Parameter + Member @@ -219,11 +229,7 @@ pub mod pallet { + FixedPointOperand; /// Handler for the unbalanced reduction when removing a dust account. - type DustRemoval: OnUnbalanced>; - - /// The overarching event type. - type RuntimeEvent: From> - + IsType<::RuntimeEvent>; + type DustRemoval: OnUnbalanced>; /// The minimum amount required to keep an account open. #[pallet::constant] @@ -232,8 +238,16 @@ pub mod pallet { /// The means of storing the balances of an account. type AccountStore: StoredMap>; - /// Weight information for extrinsics in this pallet. - type WeightInfo: WeightInfo; + /// The ID type for reserves. + /// + /// Use of reserves is deprecated in favour of holds. See `https://github.com/paritytech/substrate/pull/12951/` + type ReserveIdentifier: Parameter + Member + MaxEncodedLen + Ord + Copy; + + /// The ID type for holds. + type HoldIdentifier: Parameter + Member + MaxEncodedLen + Ord + Copy; + + /// The ID type for freezes. + type FreezeIdentifier: Parameter + Member + MaxEncodedLen + Ord + Copy; /// The maximum number of locks that should exist on an account. /// Not strictly enforced, but used for weight estimation. @@ -244,8 +258,13 @@ pub mod pallet { #[pallet::constant] type MaxReserves: Get; - /// The id type for named reserves. - type ReserveIdentifier: Parameter + Member + MaxEncodedLen + Ord + Copy; + /// The maximum number of holds that can exist on an account at any time. + #[pallet::constant] + type MaxHolds: Get; + + /// The maximum number of individual freeze locks that can exist on an account at any time. + #[pallet::constant] + type MaxFreezes: Get; } /// The current storage version. @@ -256,197 +275,6 @@ pub mod pallet { #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(PhantomData<(T, I)>); - #[pallet::call] - impl, I: 'static> Pallet { - /// Transfer some liquid free balance to another account. - /// - /// `transfer` will set the `FreeBalance` of the sender and receiver. - /// If the sender's account is below the existential deposit as a result - /// of the transfer, the account will be reaped. - /// - /// The dispatch origin for this call must be `Signed` by the transactor. - /// - /// ## Complexity - /// - Dependent on arguments but not critical, given proper implementations for input config - /// types. See related functions below. - /// - It contains a limited number of reads and writes internally and no complex - /// computation. - /// - /// Related functions: - /// - /// - `ensure_can_withdraw` is always called internally but has a bounded complexity. - /// - Transferring balances to accounts that did not exist before will cause - /// `T::OnNewAccount::on_new_account` to be called. - /// - Removing enough funds from an account will trigger `T::DustRemoval::on_unbalanced`. - /// - `transfer_keep_alive` works the same way as `transfer`, but has an additional check - /// that the transfer will not kill the origin account. - #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::transfer())] - pub fn transfer( - origin: OriginFor, - dest: AccountIdLookupOf, - #[pallet::compact] value: T::Balance, - ) -> DispatchResultWithPostInfo { - let transactor = ensure_signed(origin)?; - let dest = T::Lookup::lookup(dest)?; - >::transfer( - &transactor, - &dest, - value, - ExistenceRequirement::AllowDeath, - )?; - Ok(().into()) - } - - /// Set the balances of a given account. - /// - /// This will alter `FreeBalance` and `ReservedBalance` in storage. it will - /// also alter the total issuance of the system (`TotalIssuance`) appropriately. - /// If the new free or reserved balance is below the existential deposit, - /// it will reset the account nonce (`frame_system::AccountNonce`). - /// - /// The dispatch origin for this call is `root`. - #[pallet::call_index(1)] - #[pallet::weight( - T::WeightInfo::set_balance_creating() // Creates a new account. - .max(T::WeightInfo::set_balance_killing()) // Kills an existing account. - )] - pub fn set_balance( - origin: OriginFor, - who: AccountIdLookupOf, - #[pallet::compact] new_free: T::Balance, - #[pallet::compact] new_reserved: T::Balance, - ) -> DispatchResultWithPostInfo { - ensure_root(origin)?; - let who = T::Lookup::lookup(who)?; - let existential_deposit = T::ExistentialDeposit::get(); - - let wipeout = new_free + new_reserved < existential_deposit; - let new_free = if wipeout { Zero::zero() } else { new_free }; - let new_reserved = if wipeout { Zero::zero() } else { new_reserved }; - - // First we try to modify the account's balance to the forced balance. - let (old_free, old_reserved) = Self::mutate_account(&who, |account| { - let old_free = account.free; - let old_reserved = account.reserved; - - account.free = new_free; - account.reserved = new_reserved; - - (old_free, old_reserved) - })?; - - // This will adjust the total issuance, which was not done by the `mutate_account` - // above. - if new_free > old_free { - mem::drop(PositiveImbalance::::new(new_free - old_free)); - } else if new_free < old_free { - mem::drop(NegativeImbalance::::new(old_free - new_free)); - } - - if new_reserved > old_reserved { - mem::drop(PositiveImbalance::::new(new_reserved - old_reserved)); - } else if new_reserved < old_reserved { - mem::drop(NegativeImbalance::::new(old_reserved - new_reserved)); - } - - Self::deposit_event(Event::BalanceSet { who, free: new_free, reserved: new_reserved }); - Ok(().into()) - } - - /// Exactly as `transfer`, except the origin must be root and the source account may be - /// specified. - /// ## Complexity - /// - Same as transfer, but additional read and write because the source account is not - /// assumed to be in the overlay. - #[pallet::call_index(2)] - #[pallet::weight(T::WeightInfo::force_transfer())] - pub fn force_transfer( - origin: OriginFor, - source: AccountIdLookupOf, - dest: AccountIdLookupOf, - #[pallet::compact] value: T::Balance, - ) -> DispatchResultWithPostInfo { - ensure_root(origin)?; - let source = T::Lookup::lookup(source)?; - let dest = T::Lookup::lookup(dest)?; - >::transfer( - &source, - &dest, - value, - ExistenceRequirement::AllowDeath, - )?; - Ok(().into()) - } - - /// Same as the [`transfer`] call, but with a check that the transfer will not kill the - /// origin account. - /// - /// 99% of the time you want [`transfer`] instead. - /// - /// [`transfer`]: struct.Pallet.html#method.transfer - #[pallet::call_index(3)] - #[pallet::weight(T::WeightInfo::transfer_keep_alive())] - pub fn transfer_keep_alive( - origin: OriginFor, - dest: AccountIdLookupOf, - #[pallet::compact] value: T::Balance, - ) -> DispatchResultWithPostInfo { - let transactor = ensure_signed(origin)?; - let dest = T::Lookup::lookup(dest)?; - >::transfer(&transactor, &dest, value, KeepAlive)?; - Ok(().into()) - } - - /// Transfer the entire transferable balance from the caller account. - /// - /// NOTE: This function only attempts to transfer _transferable_ balances. This means that - /// any locked, reserved, or existential deposits (when `keep_alive` is `true`), will not be - /// transferred by this function. To ensure that this function results in a killed account, - /// you might need to prepare the account by removing any reference counters, storage - /// deposits, etc... - /// - /// The dispatch origin of this call must be Signed. - /// - /// - `dest`: The recipient of the transfer. - /// - `keep_alive`: A boolean to determine if the `transfer_all` operation should send all - /// of the funds the account has, causing the sender account to be killed (false), or - /// transfer everything except at least the existential deposit, which will guarantee to - /// keep the sender account alive (true). ## Complexity - /// - O(1). Just like transfer, but reading the user's transferable balance first. - #[pallet::call_index(4)] - #[pallet::weight(T::WeightInfo::transfer_all())] - pub fn transfer_all( - origin: OriginFor, - dest: AccountIdLookupOf, - keep_alive: bool, - ) -> DispatchResult { - use fungible::Inspect; - let transactor = ensure_signed(origin)?; - let reducible_balance = Self::reducible_balance(&transactor, keep_alive); - let dest = T::Lookup::lookup(dest)?; - let keep_alive = if keep_alive { KeepAlive } else { AllowDeath }; - >::transfer(&transactor, &dest, reducible_balance, keep_alive)?; - Ok(()) - } - - /// Unreserve some balance from a user by force. - /// - /// Can only be called by ROOT. - #[pallet::call_index(5)] - #[pallet::weight(T::WeightInfo::force_unreserve())] - pub fn force_unreserve( - origin: OriginFor, - who: AccountIdLookupOf, - amount: T::Balance, - ) -> DispatchResult { - ensure_root(origin)?; - let who = T::Lookup::lookup(who)?; - let _leftover = >::unreserve(&who, amount); - Ok(()) - } - } - #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event, I: 'static = ()> { @@ -458,7 +286,7 @@ pub mod pallet { /// Transfer succeeded. Transfer { from: T::AccountId, to: T::AccountId, amount: T::Balance }, /// A balance was set by root. - BalanceSet { who: T::AccountId, free: T::Balance, reserved: T::Balance }, + BalanceSet { who: T::AccountId, free: T::Balance }, /// Some balance was reserved (moved from free to reserved). Reserved { who: T::AccountId, amount: T::Balance }, /// Some balance was unreserved (moved from reserved to free). @@ -477,26 +305,44 @@ pub mod pallet { Withdraw { who: T::AccountId, amount: T::Balance }, /// Some amount was removed from the account (e.g. for misbehavior). Slashed { who: T::AccountId, amount: T::Balance }, + /// Some amount was minted into an account. + Minted { who: T::AccountId, amount: T::Balance }, + /// Some amount was burned from an account. + Burned { who: T::AccountId, amount: T::Balance }, + /// Some amount was suspended from an account (it can be restored later). + Suspended { who: T::AccountId, amount: T::Balance }, + /// Some amount was restored into an account. + Restored { who: T::AccountId, amount: T::Balance }, + /// An account was upgraded. + Upgraded { who: T::AccountId }, + /// Total issuance was increased by `amount`, creating a credit to be balanced. + Issued { amount: T::Balance }, + /// Total issuance was decreased by `amount`, creating a debt to be balanced. + Rescinded { amount: T::Balance }, } #[pallet::error] pub enum Error { - /// Vesting balance too high to send value + /// Vesting balance too high to send value. VestingBalance, - /// Account liquidity restrictions prevent withdrawal + /// Account liquidity restrictions prevent withdrawal. LiquidityRestrictions, /// Balance too low to send value. InsufficientBalance, - /// Value too low to create account due to existential deposit + /// Value too low to create account due to existential deposit. ExistentialDeposit, - /// Transfer/payment would kill account - KeepAlive, - /// A vesting schedule already exists for this account + /// Transfer/payment would kill account. + Expendability, + /// A vesting schedule already exists for this account. ExistingVestingSchedule, - /// Beneficiary account must pre-exist + /// Beneficiary account must pre-exist. DeadAccount, - /// Number of named reserves exceed MaxReserves + /// Number of named reserves exceed `MaxReserves`. TooManyReserves, + /// Number of holds exceed `MaxHolds`. + TooManyHolds, + /// Number of freezes exceed `MaxFreezes`. + TooManyFreezes, } /// The total units issued in the system. @@ -563,6 +409,26 @@ pub mod pallet { ValueQuery, >; + /// Holds on account balances. + #[pallet::storage] + pub type Holds, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + BoundedVec, T::MaxHolds>, + ValueQuery, + >; + + /// Freeze locks on account balances. + #[pallet::storage] + pub type Freezes, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + BoundedVec, T::MaxFreezes>, + ValueQuery, + >; + #[pallet::genesis_config] pub struct GenesisConfig, I: 'static = ()> { pub balances: Vec<(T::AccountId, T::Balance)>, @@ -602,1601 +468,646 @@ pub mod pallet { ); for &(ref who, free) in self.balances.iter() { + frame_system::Pallet::::inc_providers(who); assert!(T::AccountStore::insert(who, AccountData { free, ..Default::default() }) .is_ok()); } } } -} - -#[cfg(feature = "std")] -impl, I: 'static> GenesisConfig { - /// Direct implementation of `GenesisBuild::build_storage`. - /// - /// Kept in order not to break dependency. - pub fn build_storage(&self) -> Result { - >::build_storage(self) - } - - /// Direct implementation of `GenesisBuild::assimilate_storage`. - /// - /// Kept in order not to break dependency. - pub fn assimilate_storage(&self, storage: &mut sp_runtime::Storage) -> Result<(), String> { - >::assimilate_storage(self, storage) - } -} - -/// Simplified reasons for withdrawing balance. -#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] -pub enum Reasons { - /// Paying system transaction fees. - Fee = 0, - /// Any reason other than paying system transaction fees. - Misc = 1, - /// Any reason at all. - All = 2, -} - -impl From for Reasons { - fn from(r: WithdrawReasons) -> Reasons { - if r == WithdrawReasons::TRANSACTION_PAYMENT { - Reasons::Fee - } else if r.contains(WithdrawReasons::TRANSACTION_PAYMENT) { - Reasons::All - } else { - Reasons::Misc - } - } -} -impl BitOr for Reasons { - type Output = Reasons; - fn bitor(self, other: Reasons) -> Reasons { - if self == other { - return self + #[cfg(feature = "std")] + impl, I: 'static> GenesisConfig { + /// Direct implementation of `GenesisBuild::build_storage`. + /// + /// Kept in order not to break dependency. + pub fn build_storage(&self) -> Result { + >::build_storage(self) } - Reasons::All - } -} - -/// A single lock on a balance. There can be many of these on an account and they "overlap", so the -/// same balance is frozen by multiple locks. -#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] -pub struct BalanceLock { - /// An identifier for this lock. Only one lock may be in existence for each identifier. - pub id: LockIdentifier, - /// The amount which the free balance may not drop below when this lock is in effect. - pub amount: Balance, - /// If true, then the lock remains in effect even for payment of transaction fees. - pub reasons: Reasons, -} - -/// Store named reserved balance. -#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] -pub struct ReserveData { - /// The identifier for the named reserve. - pub id: ReserveIdentifier, - /// The amount of the named reserve. - pub amount: Balance, -} - -/// All balance information for an account. -#[derive(Encode, Decode, Clone, PartialEq, Eq, Default, RuntimeDebug, MaxEncodedLen, TypeInfo)] -pub struct AccountData { - /// Non-reserved part of the balance. There may still be restrictions on this, but it is the - /// total pool what may in principle be transferred, reserved and used for tipping. - /// - /// This is the only balance that matters in terms of most operations on tokens. It - /// alone is used to determine the balance when in the contract execution environment. - pub free: Balance, - /// Balance which is reserved and may not be used at all. - /// - /// This can still get slashed, but gets slashed last of all. - /// - /// This balance is a 'reserve' balance that other subsystems use in order to set aside tokens - /// that are still 'owned' by the account holder, but which are suspendable. - /// This includes named reserve and unnamed reserve. - pub reserved: Balance, - /// The amount that `free` may not drop below when withdrawing for *anything except transaction - /// fee payment*. - pub misc_frozen: Balance, - /// The amount that `free` may not drop below when withdrawing specifically for transaction - /// fee payment. - pub fee_frozen: Balance, -} -impl AccountData { - /// How much this account's balance can be reduced for the given `reasons`. - fn usable(&self, reasons: Reasons) -> Balance { - self.free.saturating_sub(self.frozen(reasons)) - } - /// The amount that this account's free balance may not be reduced beyond for the given - /// `reasons`. - fn frozen(&self, reasons: Reasons) -> Balance { - match reasons { - Reasons::All => self.misc_frozen.max(self.fee_frozen), - Reasons::Misc => self.misc_frozen, - Reasons::Fee => self.fee_frozen, + /// Direct implementation of `GenesisBuild::assimilate_storage`. + /// + /// Kept in order not to break dependency. + pub fn assimilate_storage(&self, storage: &mut sp_runtime::Storage) -> Result<(), String> { + >::assimilate_storage(self, storage) } } - /// The total balance in this account including any that is reserved and ignoring any frozen. - fn total(&self) -> Balance { - self.free.saturating_add(self.reserved) - } -} -pub struct DustCleaner, I: 'static = ()>( - Option<(T::AccountId, NegativeImbalance)>, -); - -impl, I: 'static> Drop for DustCleaner { - fn drop(&mut self) { - if let Some((who, dust)) = self.0.take() { - Pallet::::deposit_event(Event::DustLost { account: who, amount: dust.peek() }); - T::DustRemoval::on_unbalanced(dust); + #[pallet::call] + impl, I: 'static> Pallet { + /// Transfer some liquid free balance to another account. + /// + /// `transfer_allow_death` will set the `FreeBalance` of the sender and receiver. + /// If the sender's account is below the existential deposit as a result + /// of the transfer, the account will be reaped. + /// + /// The dispatch origin for this call must be `Signed` by the transactor. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::transfer_allow_death())] + pub fn transfer_allow_death( + origin: OriginFor, + dest: AccountIdLookupOf, + #[pallet::compact] value: T::Balance, + ) -> DispatchResultWithPostInfo { + let source = ensure_signed(origin)?; + let dest = T::Lookup::lookup(dest)?; + >::transfer(&source, &dest, value, Expendable)?; + Ok(().into()) } - } -} -impl, I: 'static> Pallet { - /// Get the free balance of an account. - pub fn free_balance(who: impl sp_std::borrow::Borrow) -> T::Balance { - Self::account(who.borrow()).free - } + /// Set the regular balance of a given account; it also takes a reserved balance but this + /// must be the same as the account's current reserved balance. + /// + /// The dispatch origin for this call is `root`. + /// + /// WARNING: This call is DEPRECATED! Use `force_set_balance` instead. + #[pallet::call_index(1)] + #[pallet::weight( + T::WeightInfo::force_set_balance_creating() // Creates a new account. + .max(T::WeightInfo::force_set_balance_killing()) // Kills an existing account. + )] + pub fn set_balance_deprecated( + origin: OriginFor, + who: AccountIdLookupOf, + #[pallet::compact] new_free: T::Balance, + #[pallet::compact] old_reserved: T::Balance, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + let who = T::Lookup::lookup(who)?; + let existential_deposit = T::ExistentialDeposit::get(); - /// Get the balance of an account that can be used for transfers, reservations, or any other - /// non-locking, non-transaction-fee activity. Will be at most `free_balance`. - pub fn usable_balance(who: impl sp_std::borrow::Borrow) -> T::Balance { - Self::account(who.borrow()).usable(Reasons::Misc) - } + let wipeout = new_free < existential_deposit; + let new_free = if wipeout { Zero::zero() } else { new_free }; - /// Get the balance of an account that can be used for paying transaction fees (not tipping, - /// or any other kind of fees, though). Will be at most `free_balance`. - pub fn usable_balance_for_fees(who: impl sp_std::borrow::Borrow) -> T::Balance { - Self::account(who.borrow()).usable(Reasons::Fee) - } + // First we try to modify the account's balance to the forced balance. + let old_free = Self::try_mutate_account_handling_dust( + &who, + |account, _is_new| -> Result { + let old_free = account.free; + ensure!(account.reserved == old_reserved, TokenError::Unsupported); + account.free = new_free; + Ok(old_free) + }, + )?; - /// Get the reserved balance of an account. - pub fn reserved_balance(who: impl sp_std::borrow::Borrow) -> T::Balance { - Self::account(who.borrow()).reserved - } + // This will adjust the total issuance, which was not done by the `mutate_account` + // above. + if new_free > old_free { + mem::drop(PositiveImbalance::::new(new_free - old_free)); + } else if new_free < old_free { + mem::drop(NegativeImbalance::::new(old_free - new_free)); + } - /// Get both the free and reserved balances of an account. - fn account(who: &T::AccountId) -> AccountData { - T::AccountStore::get(who) - } + Self::deposit_event(Event::BalanceSet { who, free: new_free }); + Ok(().into()) + } - /// Handles any steps needed after mutating an account. - /// - /// This includes DustRemoval unbalancing, in the case than the `new` account's total balance - /// is non-zero but below ED. - /// - /// Returns two values: - /// - `Some` containing the the `new` account, iff the account has sufficient balance. - /// - `Some` containing the dust to be dropped, iff some dust should be dropped. - fn post_mutation( - _who: &T::AccountId, - new: AccountData, - ) -> (Option>, Option>) { - let total = new.total(); - if total < T::ExistentialDeposit::get() { - if total.is_zero() { - (None, None) - } else { - (None, Some(NegativeImbalance::new(total))) - } - } else { - (Some(new), None) - } - } - - fn deposit_consequence( - _who: &T::AccountId, - amount: T::Balance, - account: &AccountData, - mint: bool, - ) -> DepositConsequence { - if amount.is_zero() { - return DepositConsequence::Success - } - - if mint && TotalIssuance::::get().checked_add(&amount).is_none() { - return DepositConsequence::Overflow - } - - let new_total_balance = match account.total().checked_add(&amount) { - Some(x) => x, - None => return DepositConsequence::Overflow, - }; - - if new_total_balance < T::ExistentialDeposit::get() { - return DepositConsequence::BelowMinimum - } - - // NOTE: We assume that we are a provider, so don't need to do any checks in the - // case of account creation. - - DepositConsequence::Success - } - - fn withdraw_consequence( - who: &T::AccountId, - amount: T::Balance, - account: &AccountData, - ) -> WithdrawConsequence { - if amount.is_zero() { - return WithdrawConsequence::Success - } - - if TotalIssuance::::get().checked_sub(&amount).is_none() { - return WithdrawConsequence::Underflow + /// Exactly as `transfer_allow_death`, except the origin must be root and the source account + /// may be specified. + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::force_transfer())] + pub fn force_transfer( + origin: OriginFor, + source: AccountIdLookupOf, + dest: AccountIdLookupOf, + #[pallet::compact] value: T::Balance, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + let source = T::Lookup::lookup(source)?; + let dest = T::Lookup::lookup(dest)?; + >::transfer(&source, &dest, value, Expendable)?; + Ok(().into()) } - let new_total_balance = match account.total().checked_sub(&amount) { - Some(x) => x, - None => return WithdrawConsequence::NoFunds, - }; - - // Provider restriction - total account balance cannot be reduced to zero if it cannot - // sustain the loss of a provider reference. - // NOTE: This assumes that the pallet is a provider (which is true). Is this ever changes, - // then this will need to adapt accordingly. - let ed = T::ExistentialDeposit::get(); - let success = if new_total_balance < ed { - if frame_system::Pallet::::can_dec_provider(who) { - WithdrawConsequence::ReducedToZero(new_total_balance) - } else { - return WithdrawConsequence::WouldDie - } - } else { - WithdrawConsequence::Success - }; - - // Enough free funds to have them be reduced. - let new_free_balance = match account.free.checked_sub(&amount) { - Some(b) => b, - None => return WithdrawConsequence::NoFunds, - }; - - // Eventual free funds must be no less than the frozen balance. - let min_balance = account.frozen(Reasons::All); - if new_free_balance < min_balance { - return WithdrawConsequence::Frozen + /// Same as the [`transfer_allow_death`] call, but with a check that the transfer will not + /// kill the origin account. + /// + /// 99% of the time you want [`transfer_allow_death`] instead. + /// + /// [`transfer_allow_death`]: struct.Pallet.html#method.transfer + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::transfer_keep_alive())] + pub fn transfer_keep_alive( + origin: OriginFor, + dest: AccountIdLookupOf, + #[pallet::compact] value: T::Balance, + ) -> DispatchResultWithPostInfo { + let source = ensure_signed(origin)?; + let dest = T::Lookup::lookup(dest)?; + >::transfer(&source, &dest, value, Preserve)?; + Ok(().into()) } - success - } - - /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce - /// `ExistentialDeposit` law, annulling the account as needed. - /// - /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used - /// when it is known that the account already exists. - /// - /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that - /// the caller will do this. - pub fn mutate_account( - who: &T::AccountId, - f: impl FnOnce(&mut AccountData) -> R, - ) -> Result { - Self::try_mutate_account(who, |a, _| -> Result { Ok(f(a)) }) - } - - /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce - /// `ExistentialDeposit` law, annulling the account as needed. This will do nothing if the - /// result of `f` is an `Err`. - /// - /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used - /// when it is known that the account already exists. - /// - /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that - /// the caller will do this. - fn try_mutate_account>( - who: &T::AccountId, - f: impl FnOnce(&mut AccountData, bool) -> Result, - ) -> Result { - Self::try_mutate_account_with_dust(who, f).map(|(result, dust_cleaner)| { - drop(dust_cleaner); - result - }) - } - - /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce - /// `ExistentialDeposit` law, annulling the account as needed. This will do nothing if the - /// result of `f` is an `Err`. - /// - /// It returns both the result from the closure, and an optional `DustCleaner` instance which - /// should be dropped once it is known that all nested mutates that could affect storage items - /// what the dust handler touches have completed. - /// - /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used - /// when it is known that the account already exists. - /// - /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that - /// the caller will do this. - fn try_mutate_account_with_dust>( - who: &T::AccountId, - f: impl FnOnce(&mut AccountData, bool) -> Result, - ) -> Result<(R, DustCleaner), E> { - let result = T::AccountStore::try_mutate_exists(who, |maybe_account| { - let is_new = maybe_account.is_none(); - let mut account = maybe_account.take().unwrap_or_default(); - f(&mut account, is_new).map(move |result| { - let maybe_endowed = if is_new { Some(account.free) } else { None }; - let maybe_account_maybe_dust = Self::post_mutation(who, account); - *maybe_account = maybe_account_maybe_dust.0; - (maybe_endowed, maybe_account_maybe_dust.1, result) - }) - }); - result.map(|(maybe_endowed, maybe_dust, result)| { - if let Some(endowed) = maybe_endowed { - Self::deposit_event(Event::Endowed { account: who.clone(), free_balance: endowed }); - } - let dust_cleaner = DustCleaner(maybe_dust.map(|dust| (who.clone(), dust))); - (result, dust_cleaner) - }) - } - - /// Update the account entry for `who`, given the locks. - fn update_locks(who: &T::AccountId, locks: &[BalanceLock]) { - let bounded_locks = WeakBoundedVec::<_, T::MaxLocks>::force_from( - locks.to_vec(), - Some("Balances Update Locks"), - ); - - if locks.len() as u32 > T::MaxLocks::get() { - log::warn!( - target: LOG_TARGET, - "Warning: A user has more currency locks than expected. \ - A runtime configuration adjustment may be needed." + /// Transfer the entire transferable balance from the caller account. + /// + /// NOTE: This function only attempts to transfer _transferable_ balances. This means that + /// any locked, reserved, or existential deposits (when `keep_alive` is `true`), will not be + /// transferred by this function. To ensure that this function results in a killed account, + /// you might need to prepare the account by removing any reference counters, storage + /// deposits, etc... + /// + /// The dispatch origin of this call must be Signed. + /// + /// - `dest`: The recipient of the transfer. + /// - `keep_alive`: A boolean to determine if the `transfer_all` operation should send all + /// of the funds the account has, causing the sender account to be killed (false), or + /// transfer everything except at least the existential deposit, which will guarantee to + /// keep the sender account alive (true). + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::transfer_all())] + pub fn transfer_all( + origin: OriginFor, + dest: AccountIdLookupOf, + keep_alive: bool, + ) -> DispatchResult { + let transactor = ensure_signed(origin)?; + let keep_alive = if keep_alive { Preserve } else { Expendable }; + let reducible_balance = >::reducible_balance( + &transactor, + keep_alive, + Fortitude::Polite, ); - } - // No way this can fail since we do not alter the existential balances. - let res = Self::mutate_account(who, |b| { - b.misc_frozen = Zero::zero(); - b.fee_frozen = Zero::zero(); - for l in locks.iter() { - if l.reasons == Reasons::All || l.reasons == Reasons::Misc { - b.misc_frozen = b.misc_frozen.max(l.amount); - } - if l.reasons == Reasons::All || l.reasons == Reasons::Fee { - b.fee_frozen = b.fee_frozen.max(l.amount); - } - } - }); - debug_assert!(res.is_ok()); - - let existed = Locks::::contains_key(who); - if locks.is_empty() { - Locks::::remove(who); - if existed { - // TODO: use Locks::::hashed_key - // https://github.com/paritytech/substrate/issues/4969 - system::Pallet::::dec_consumers(who); - } - } else { - Locks::::insert(who, bounded_locks); - if !existed && system::Pallet::::inc_consumers_without_limit(who).is_err() { - // No providers for the locks. This is impossible under normal circumstances - // since the funds that are under the lock will themselves be stored in the - // account and therefore will need a reference. - log::warn!( - target: LOG_TARGET, - "Warning: Attempt to introduce lock consumer reference, yet no providers. \ - This is unexpected but should be safe." - ); - } - } - } - - /// Move the reserved balance of one account into the balance of another, according to `status`. - /// - /// Is a no-op if: - /// - the value to be moved is zero; or - /// - the `slashed` id equal to `beneficiary` and the `status` is `Reserved`. - /// - /// NOTE: returns actual amount of transferred value in `Ok` case. - fn do_transfer_reserved( - slashed: &T::AccountId, - beneficiary: &T::AccountId, - value: T::Balance, - best_effort: bool, - status: Status, - ) -> Result { - if value.is_zero() { - return Ok(Zero::zero()) - } - - if slashed == beneficiary { - return match status { - Status::Free => Ok(value.saturating_sub(Self::unreserve(slashed, value))), - Status::Reserved => Ok(value.saturating_sub(Self::reserved_balance(slashed))), - } - } - - let ((actual, _maybe_one_dust), _maybe_other_dust) = Self::try_mutate_account_with_dust( - beneficiary, - |to_account, is_new| -> Result<(T::Balance, DustCleaner), DispatchError> { - ensure!(!is_new, Error::::DeadAccount); - Self::try_mutate_account_with_dust( - slashed, - |from_account, _| -> Result { - let actual = cmp::min(from_account.reserved, value); - ensure!(best_effort || actual == value, Error::::InsufficientBalance); - match status { - Status::Free => - to_account.free = to_account - .free - .checked_add(&actual) - .ok_or(ArithmeticError::Overflow)?, - Status::Reserved => - to_account.reserved = to_account - .reserved - .checked_add(&actual) - .ok_or(ArithmeticError::Overflow)?, - } - from_account.reserved -= actual; - Ok(actual) - }, - ) - }, - )?; - - Self::deposit_event(Event::ReserveRepatriated { - from: slashed.clone(), - to: beneficiary.clone(), - amount: actual, - destination_status: status, - }); - Ok(actual) - } -} - -impl, I: 'static> fungible::Inspect for Pallet { - type Balance = T::Balance; - - fn total_issuance() -> Self::Balance { - TotalIssuance::::get() - } - fn active_issuance() -> Self::Balance { - TotalIssuance::::get().saturating_sub(InactiveIssuance::::get()) - } - fn minimum_balance() -> Self::Balance { - T::ExistentialDeposit::get() - } - fn balance(who: &T::AccountId) -> Self::Balance { - Self::account(who).total() - } - fn reducible_balance(who: &T::AccountId, keep_alive: bool) -> Self::Balance { - let a = Self::account(who); - // Liquid balance is what is neither reserved nor locked/frozen. - let liquid = a.free.saturating_sub(a.fee_frozen.max(a.misc_frozen)); - if frame_system::Pallet::::can_dec_provider(who) && !keep_alive { - liquid - } else { - // `must_remain_to_exist` is the part of liquid balance which must remain to keep total - // over ED. - let must_remain_to_exist = - T::ExistentialDeposit::get().saturating_sub(a.total() - liquid); - liquid.saturating_sub(must_remain_to_exist) - } - } - fn can_deposit(who: &T::AccountId, amount: Self::Balance, mint: bool) -> DepositConsequence { - Self::deposit_consequence(who, amount, &Self::account(who), mint) - } - fn can_withdraw( - who: &T::AccountId, - amount: Self::Balance, - ) -> WithdrawConsequence { - Self::withdraw_consequence(who, amount, &Self::account(who)) - } -} - -impl, I: 'static> fungible::Mutate for Pallet { - fn mint_into(who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - if amount.is_zero() { - return Ok(()) - } - Self::try_mutate_account(who, |account, _is_new| -> DispatchResult { - Self::deposit_consequence(who, amount, account, true).into_result()?; - account.free += amount; + let dest = T::Lookup::lookup(dest)?; + >::transfer( + &transactor, + &dest, + reducible_balance, + keep_alive, + )?; Ok(()) - })?; - TotalIssuance::::mutate(|t| *t += amount); - Self::deposit_event(Event::Deposit { who: who.clone(), amount }); - Ok(()) - } - - fn burn_from( - who: &T::AccountId, - amount: Self::Balance, - ) -> Result { - if amount.is_zero() { - return Ok(Self::Balance::zero()) } - let actual = Self::try_mutate_account( - who, - |account, _is_new| -> Result { - let extra = Self::withdraw_consequence(who, amount, account).into_result()?; - let actual = amount + extra; - account.free -= actual; - Ok(actual) - }, - )?; - TotalIssuance::::mutate(|t| *t -= actual); - Self::deposit_event(Event::Withdraw { who: who.clone(), amount }); - Ok(actual) - } -} - -impl, I: 'static> fungible::Transfer for Pallet { - fn transfer( - source: &T::AccountId, - dest: &T::AccountId, - amount: T::Balance, - keep_alive: bool, - ) -> Result { - let er = if keep_alive { KeepAlive } else { AllowDeath }; - >::transfer(source, dest, amount, er).map(|_| amount) - } - - fn deactivate(amount: Self::Balance) { - InactiveIssuance::::mutate(|b| b.saturating_accrue(amount)); - } - - fn reactivate(amount: Self::Balance) { - InactiveIssuance::::mutate(|b| b.saturating_reduce(amount)); - } -} - -impl, I: 'static> fungible::Unbalanced for Pallet { - fn set_balance(who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - Self::mutate_account(who, |account| -> DispatchResult { - // fungibles::Unbalanced::decrease_balance didn't check account.reserved - // free = new_balance - reserved - account.free = - amount.checked_sub(&account.reserved).ok_or(ArithmeticError::Underflow)?; - Self::deposit_event(Event::BalanceSet { - who: who.clone(), - free: account.free, - reserved: account.reserved, - }); + /// Unreserve some balance from a user by force. + /// + /// Can only be called by ROOT. + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::force_unreserve())] + pub fn force_unreserve( + origin: OriginFor, + who: AccountIdLookupOf, + amount: T::Balance, + ) -> DispatchResult { + ensure_root(origin)?; + let who = T::Lookup::lookup(who)?; + let _leftover = >::unreserve(&who, amount); Ok(()) - })? - } - - fn set_total_issuance(amount: Self::Balance) { - TotalIssuance::::mutate(|t| *t = amount); - } -} - -impl, I: 'static> fungible::InspectHold for Pallet { - fn balance_on_hold(who: &T::AccountId) -> T::Balance { - Self::account(who).reserved - } - fn can_hold(who: &T::AccountId, amount: T::Balance) -> bool { - let a = Self::account(who); - let min_balance = T::ExistentialDeposit::get().max(a.frozen(Reasons::All)); - if a.reserved.checked_add(&amount).is_none() { - return false - } - // We require it to be min_balance + amount to ensure that the full reserved funds may be - // slashed without compromising locked funds or destroying the account. - let required_free = match min_balance.checked_add(&amount) { - Some(x) => x, - None => return false, - }; - a.free >= required_free - } -} -impl, I: 'static> fungible::MutateHold for Pallet { - fn hold(who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - if amount.is_zero() { - return Ok(()) - } - ensure!(Self::can_reserve(who, amount), Error::::InsufficientBalance); - Self::mutate_account(who, |a| { - a.free -= amount; - a.reserved += amount; - })?; - Ok(()) - } - fn release( - who: &T::AccountId, - amount: Self::Balance, - best_effort: bool, - ) -> Result { - if amount.is_zero() { - return Ok(amount) } - // Done on a best-effort basis. - Self::try_mutate_account(who, |a, _| { - let new_free = a.free.saturating_add(amount.min(a.reserved)); - let actual = new_free - a.free; - ensure!(best_effort || actual == amount, Error::::InsufficientBalance); - // ^^^ Guaranteed to be <= amount and <= a.reserved - a.free = new_free; - a.reserved = a.reserved.saturating_sub(actual); - Ok(actual) - }) - } - fn transfer_held( - source: &T::AccountId, - dest: &T::AccountId, - amount: Self::Balance, - best_effort: bool, - on_hold: bool, - ) -> Result { - let status = if on_hold { Status::Reserved } else { Status::Free }; - Self::do_transfer_reserved(source, dest, amount, best_effort, status) - } -} - -// wrapping these imbalances in a private module is necessary to ensure absolute privacy -// of the inner member. -mod imbalances { - use super::{result, Config, Imbalance, RuntimeDebug, Saturating, TryDrop, Zero}; - use frame_support::traits::SameOrOther; - use sp_std::mem; - - /// Opaque, move-only struct with private fields that serves as a token denoting that - /// funds have been created without any equal and opposite accounting. - #[must_use] - #[derive(RuntimeDebug, PartialEq, Eq)] - pub struct PositiveImbalance, I: 'static = ()>(T::Balance); - - impl, I: 'static> PositiveImbalance { - /// Create a new positive imbalance from a balance. - pub fn new(amount: T::Balance) -> Self { - PositiveImbalance(amount) - } - } - /// Opaque, move-only struct with private fields that serves as a token denoting that - /// funds have been destroyed without any equal and opposite accounting. - #[must_use] - #[derive(RuntimeDebug, PartialEq, Eq)] - pub struct NegativeImbalance, I: 'static = ()>(T::Balance); - - impl, I: 'static> NegativeImbalance { - /// Create a new negative imbalance from a balance. - pub fn new(amount: T::Balance) -> Self { - NegativeImbalance(amount) - } - } - - impl, I: 'static> TryDrop for PositiveImbalance { - fn try_drop(self) -> result::Result<(), Self> { - self.drop_zero() - } - } - - impl, I: 'static> Default for PositiveImbalance { - fn default() -> Self { - Self::zero() - } - } - - impl, I: 'static> Imbalance for PositiveImbalance { - type Opposite = NegativeImbalance; - - fn zero() -> Self { - Self(Zero::zero()) - } - fn drop_zero(self) -> result::Result<(), Self> { - if self.0.is_zero() { - Ok(()) - } else { - Err(self) + /// Upgrade a specified account. + /// + /// - `origin`: Must be `Signed`. + /// - `who`: The account to be upgraded. + /// + /// This will waive the transaction fee if at least all but 10% of the accounts needed to + /// be upgraded. (We let some not have to be upgraded just in order to allow for the + /// possibililty of churn). + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::upgrade_accounts(who.len() as u32))] + pub fn upgrade_accounts( + origin: OriginFor, + who: Vec, + ) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + if who.is_empty() { + return Ok(Pays::Yes.into()) } - } - fn split(self, amount: T::Balance) -> (Self, Self) { - let first = self.0.min(amount); - let second = self.0 - first; - - mem::forget(self); - (Self(first), Self(second)) - } - fn merge(mut self, other: Self) -> Self { - self.0 = self.0.saturating_add(other.0); - mem::forget(other); - - self - } - fn subsume(&mut self, other: Self) { - self.0 = self.0.saturating_add(other.0); - mem::forget(other); - } - fn offset(self, other: Self::Opposite) -> SameOrOther { - let (a, b) = (self.0, other.0); - mem::forget((self, other)); - - if a > b { - SameOrOther::Same(Self(a - b)) - } else if b > a { - SameOrOther::Other(NegativeImbalance::new(b - a)) + let mut upgrade_count = 0; + for i in &who { + let upgraded = Self::ensure_upgraded(i); + if upgraded { + upgrade_count.saturating_inc(); + } + } + let proportion_upgraded = Perbill::from_rational(upgrade_count, who.len() as u32); + if proportion_upgraded >= Perbill::from_percent(90) { + Ok(Pays::No.into()) } else { - SameOrOther::None + Ok(Pays::Yes.into()) } } - fn peek(&self) -> T::Balance { - self.0 - } - } - - impl, I: 'static> TryDrop for NegativeImbalance { - fn try_drop(self) -> result::Result<(), Self> { - self.drop_zero() - } - } - impl, I: 'static> Default for NegativeImbalance { - fn default() -> Self { - Self::zero() + /// Alias for `transfer_allow_death`, provided only for name-wise compatibility. + /// + /// WARNING: DEPRECATED! Will be released in approximately 3 months. + #[pallet::call_index(7)] + #[pallet::weight(T::WeightInfo::transfer_allow_death())] + pub fn transfer( + origin: OriginFor, + dest: AccountIdLookupOf, + #[pallet::compact] value: T::Balance, + ) -> DispatchResultWithPostInfo { + let source = ensure_signed(origin)?; + let dest = T::Lookup::lookup(dest)?; + >::transfer(&source, &dest, value, Expendable)?; + Ok(().into()) } - } - impl, I: 'static> Imbalance for NegativeImbalance { - type Opposite = PositiveImbalance; + /// Set the regular balance of a given account. + /// + /// The dispatch origin for this call is `root`. + #[pallet::call_index(8)] + #[pallet::weight( + T::WeightInfo::force_set_balance_creating() // Creates a new account. + .max(T::WeightInfo::force_set_balance_killing()) // Kills an existing account. + )] + pub fn force_set_balance( + origin: OriginFor, + who: AccountIdLookupOf, + #[pallet::compact] new_free: T::Balance, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + let who = T::Lookup::lookup(who)?; + let existential_deposit = T::ExistentialDeposit::get(); - fn zero() -> Self { - Self(Zero::zero()) - } - fn drop_zero(self) -> result::Result<(), Self> { - if self.0.is_zero() { - Ok(()) - } else { - Err(self) - } - } - fn split(self, amount: T::Balance) -> (Self, Self) { - let first = self.0.min(amount); - let second = self.0 - first; + let wipeout = new_free < existential_deposit; + let new_free = if wipeout { Zero::zero() } else { new_free }; - mem::forget(self); - (Self(first), Self(second)) - } - fn merge(mut self, other: Self) -> Self { - self.0 = self.0.saturating_add(other.0); - mem::forget(other); + // First we try to modify the account's balance to the forced balance. + let old_free = Self::mutate_account_handling_dust(&who, |account| { + let old_free = account.free; + account.free = new_free; + old_free + })?; - self - } - fn subsume(&mut self, other: Self) { - self.0 = self.0.saturating_add(other.0); - mem::forget(other); - } - fn offset(self, other: Self::Opposite) -> SameOrOther { - let (a, b) = (self.0, other.0); - mem::forget((self, other)); - - if a > b { - SameOrOther::Same(Self(a - b)) - } else if b > a { - SameOrOther::Other(PositiveImbalance::new(b - a)) - } else { - SameOrOther::None + // This will adjust the total issuance, which was not done by the `mutate_account` + // above. + if new_free > old_free { + mem::drop(PositiveImbalance::::new(new_free - old_free)); + } else if new_free < old_free { + mem::drop(NegativeImbalance::::new(old_free - new_free)); } - } - fn peek(&self) -> T::Balance { - self.0 - } - } - impl, I: 'static> Drop for PositiveImbalance { - /// Basic drop handler will just square up the total issuance. - fn drop(&mut self) { - >::mutate(|v| *v = v.saturating_add(self.0)); - } - } - - impl, I: 'static> Drop for NegativeImbalance { - /// Basic drop handler will just square up the total issuance. - fn drop(&mut self) { - >::mutate(|v| *v = v.saturating_sub(self.0)); + Self::deposit_event(Event::BalanceSet { who, free: new_free }); + Ok(().into()) } } -} - -impl, I: 'static> Currency for Pallet -where - T::Balance: MaybeSerializeDeserialize + Debug, -{ - type Balance = T::Balance; - type PositiveImbalance = PositiveImbalance; - type NegativeImbalance = NegativeImbalance; - - fn total_balance(who: &T::AccountId) -> Self::Balance { - Self::account(who).total() - } - // Check if `value` amount of free balance can be slashed from `who`. - fn can_slash(who: &T::AccountId, value: Self::Balance) -> bool { - if value.is_zero() { + impl, I: 'static> Pallet { + /// Ensure the account `who` is using the new logic. + /// + /// Returns `true` if the account did get upgraded, `false` if it didn't need upgrading. + pub fn ensure_upgraded(who: &T::AccountId) -> bool { + let mut a = T::AccountStore::get(who); + if a.flags.is_new_logic() { + return false + } + a.flags.set_new_logic(); + if !a.reserved.is_zero() || !a.frozen.is_zero() { + if !system::Pallet::::can_inc_consumer(who) { + // Gah!! We have a non-zero reserve balance but no provider refs :( + // This shouldn't practically happen, but we need a failsafe anyway: let's give + // them enough for an ED. + a.free = a.free.min(T::ExistentialDeposit::get()); + system::Pallet::::inc_providers(who); + } + let _ = system::Pallet::::inc_consumers(who).defensive(); + } + // Should never fail - we're only setting a bit. + let _ = T::AccountStore::try_mutate_exists(who, |account| -> DispatchResult { + *account = Some(a); + Ok(()) + }); + Self::deposit_event(Event::Upgraded { who: who.clone() }); return true } - Self::free_balance(who) >= value - } - - fn total_issuance() -> Self::Balance { - TotalIssuance::::get() - } - - fn active_issuance() -> Self::Balance { - >::active_issuance() - } - - fn deactivate(amount: Self::Balance) { - >::deactivate(amount); - } - - fn reactivate(amount: Self::Balance) { - >::reactivate(amount); - } - fn minimum_balance() -> Self::Balance { - T::ExistentialDeposit::get() - } - - // Burn funds from the total issuance, returning a positive imbalance for the amount burned. - // Is a no-op if amount to be burned is zero. - fn burn(mut amount: Self::Balance) -> Self::PositiveImbalance { - if amount.is_zero() { - return PositiveImbalance::zero() + /// Get the free balance of an account. + pub fn free_balance(who: impl sp_std::borrow::Borrow) -> T::Balance { + Self::account(who.borrow()).free } - >::mutate(|issued| { - *issued = issued.checked_sub(&amount).unwrap_or_else(|| { - amount = *issued; - Zero::zero() - }); - }); - PositiveImbalance::new(amount) - } - // Create new funds into the total issuance, returning a negative imbalance - // for the amount issued. - // Is a no-op if amount to be issued it zero. - fn issue(mut amount: Self::Balance) -> Self::NegativeImbalance { - if amount.is_zero() { - return NegativeImbalance::zero() + /// Get the balance of an account that can be used for transfers, reservations, or any other + /// non-locking, non-transaction-fee activity. Will be at most `free_balance`. + pub fn usable_balance(who: impl sp_std::borrow::Borrow) -> T::Balance { + >::reducible_balance(who.borrow(), Expendable, Polite) } - >::mutate(|issued| { - *issued = issued.checked_add(&amount).unwrap_or_else(|| { - amount = Self::Balance::max_value() - *issued; - Self::Balance::max_value() - }) - }); - NegativeImbalance::new(amount) - } - - fn free_balance(who: &T::AccountId) -> Self::Balance { - Self::account(who).free - } - // Ensure that an account can withdraw from their free balance given any existing withdrawal - // restrictions like locks and vesting balance. - // Is a no-op if amount to be withdrawn is zero. - // - // ## Complexity - // Despite iterating over a list of locks, they are limited by the number of - // lock IDs, which means the number of runtime pallets that intend to use and create locks. - fn ensure_can_withdraw( - who: &T::AccountId, - amount: T::Balance, - reasons: WithdrawReasons, - new_balance: T::Balance, - ) -> DispatchResult { - if amount.is_zero() { - return Ok(()) + /// Get the balance of an account that can be used for paying transaction fees (not tipping, + /// or any other kind of fees, though). Will be at most `free_balance`. + /// + /// This requires that the account stays alive. + pub fn usable_balance_for_fees( + who: impl sp_std::borrow::Borrow, + ) -> T::Balance { + >::reducible_balance(who.borrow(), Protect, Polite) } - let min_balance = Self::account(who).frozen(reasons.into()); - ensure!(new_balance >= min_balance, Error::::LiquidityRestrictions); - Ok(()) - } - // Transfer some free balance from `transactor` to `dest`, respecting existence requirements. - // Is a no-op if value to be transferred is zero or the `transactor` is the same as `dest`. - fn transfer( - transactor: &T::AccountId, - dest: &T::AccountId, - value: Self::Balance, - existence_requirement: ExistenceRequirement, - ) -> DispatchResult { - if value.is_zero() || transactor == dest { - return Ok(()) + /// Get the reserved balance of an account. + pub fn reserved_balance(who: impl sp_std::borrow::Borrow) -> T::Balance { + Self::account(who.borrow()).reserved } - Self::try_mutate_account_with_dust( - dest, - |to_account, _| -> Result, DispatchError> { - Self::try_mutate_account_with_dust( - transactor, - |from_account, _| -> DispatchResult { - from_account.free = from_account - .free - .checked_sub(&value) - .ok_or(Error::::InsufficientBalance)?; - - // NOTE: total stake being stored in the same type means that this could - // never overflow but better to be safe than sorry. - to_account.free = - to_account.free.checked_add(&value).ok_or(ArithmeticError::Overflow)?; - - let ed = T::ExistentialDeposit::get(); - ensure!(to_account.total() >= ed, Error::::ExistentialDeposit); - - Self::ensure_can_withdraw( - transactor, - value, - WithdrawReasons::TRANSFER, - from_account.free, - ) - .map_err(|_| Error::::LiquidityRestrictions)?; - - // TODO: This is over-conservative. There may now be other providers, and - // this pallet may not even be a provider. - let allow_death = existence_requirement == ExistenceRequirement::AllowDeath; - let allow_death = - allow_death && system::Pallet::::can_dec_provider(transactor); - ensure!( - allow_death || from_account.total() >= ed, - Error::::KeepAlive - ); - - Ok(()) - }, - ) - .map(|(_, maybe_dust_cleaner)| maybe_dust_cleaner) - }, - )?; - - // Emit transfer event. - Self::deposit_event(Event::Transfer { - from: transactor.clone(), - to: dest.clone(), - amount: value, - }); - - Ok(()) - } - - /// Slash a target account `who`, returning the negative imbalance created and any left over - /// amount that could not be slashed. - /// - /// Is a no-op if `value` to be slashed is zero or the account does not exist. - /// - /// NOTE: `slash()` prefers free balance, but assumes that reserve balance can be drawn - /// from in extreme circumstances. `can_slash()` should be used prior to `slash()` to avoid - /// having to draw from reserved funds, however we err on the side of punishment if things are - /// inconsistent or `can_slash` wasn't used appropriately. - fn slash(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { - if value.is_zero() { - return (NegativeImbalance::zero(), Zero::zero()) - } - if Self::total_balance(who).is_zero() { - return (NegativeImbalance::zero(), value) + /// Get both the free and reserved balances of an account. + pub(crate) fn account(who: &T::AccountId) -> AccountData { + T::AccountStore::get(who) } - for attempt in 0..2 { - match Self::try_mutate_account( - who, - |account, - _is_new| - -> Result<(Self::NegativeImbalance, Self::Balance), DispatchError> { - // Best value is the most amount we can slash following liveness rules. - let best_value = match attempt { - // First attempt we try to slash the full amount, and see if liveness issues - // happen. - 0 => value, - // If acting as a critical provider (i.e. first attempt failed), then slash - // as much as possible while leaving at least at ED. - _ => value.min( - (account.free + account.reserved) - .saturating_sub(T::ExistentialDeposit::get()), - ), - }; - - let free_slash = cmp::min(account.free, best_value); - account.free -= free_slash; // Safe because of above check - let remaining_slash = best_value - free_slash; // Safe because of above check - - if !remaining_slash.is_zero() { - // If we have remaining slash, take it from reserved balance. - let reserved_slash = cmp::min(account.reserved, remaining_slash); - account.reserved -= reserved_slash; // Safe because of above check - Ok(( - NegativeImbalance::new(free_slash + reserved_slash), - value - free_slash - reserved_slash, /* Safe because value is gt or - * eq total slashed */ - )) - } else { - // Else we are done! - Ok(( - NegativeImbalance::new(free_slash), - value - free_slash, // Safe because value is gt or eq to total slashed - )) - } - }, - ) { - Ok((imbalance, not_slashed)) => { - Self::deposit_event(Event::Slashed { - who: who.clone(), - amount: value.saturating_sub(not_slashed), - }); - return (imbalance, not_slashed) - }, - Err(_) => (), + /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce + /// `ExistentialDeposit` law, annulling the account as needed. + /// + /// It returns the result from the closure. Any dust is handled through the low-level + /// `fungible::Unbalanced` trap-door for legacy dust management. + /// + /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used + /// when it is known that the account already exists. + /// + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that + /// the caller will do this. + pub(crate) fn mutate_account_handling_dust( + who: &T::AccountId, + f: impl FnOnce(&mut AccountData) -> R, + ) -> Result { + let (r, maybe_dust) = Self::mutate_account(who, f)?; + if let Some(dust) = maybe_dust { + >::handle_raw_dust(dust); } + Ok(r) } - // Should never get here. But we'll be defensive anyway. - (Self::NegativeImbalance::zero(), value) - } - - /// Deposit some `value` into the free balance of an existing target account `who`. - /// - /// Is a no-op if the `value` to be deposited is zero. - fn deposit_into_existing( - who: &T::AccountId, - value: Self::Balance, - ) -> Result { - if value.is_zero() { - return Ok(PositiveImbalance::zero()) - } - - Self::try_mutate_account( - who, - |account, is_new| -> Result { - ensure!(!is_new, Error::::DeadAccount); - account.free = account.free.checked_add(&value).ok_or(ArithmeticError::Overflow)?; - Self::deposit_event(Event::Deposit { who: who.clone(), amount: value }); - Ok(PositiveImbalance::new(value)) - }, - ) - } - - /// Deposit some `value` into the free balance of `who`, possibly creating a new account. - /// - /// This function is a no-op if: - /// - the `value` to be deposited is zero; or - /// - the `value` to be deposited is less than the required ED and the account does not yet - /// exist; or - /// - the deposit would necessitate the account to exist and there are no provider references; - /// or - /// - `value` is so large it would cause the balance of `who` to overflow. - fn deposit_creating(who: &T::AccountId, value: Self::Balance) -> Self::PositiveImbalance { - if value.is_zero() { - return Self::PositiveImbalance::zero() - } - - Self::try_mutate_account( - who, - |account, is_new| -> Result { - let ed = T::ExistentialDeposit::get(); - ensure!(value >= ed || !is_new, Error::::ExistentialDeposit); - - // defensive only: overflow should never happen, however in case it does, then this - // operation is a no-op. - account.free = match account.free.checked_add(&value) { - Some(x) => x, - None => return Ok(Self::PositiveImbalance::zero()), - }; - - Self::deposit_event(Event::Deposit { who: who.clone(), amount: value }); - Ok(PositiveImbalance::new(value)) - }, - ) - .unwrap_or_else(|_| Self::PositiveImbalance::zero()) - } - - /// Withdraw some free balance from an account, respecting existence requirements. - /// - /// Is a no-op if value to be withdrawn is zero. - fn withdraw( - who: &T::AccountId, - value: Self::Balance, - reasons: WithdrawReasons, - liveness: ExistenceRequirement, - ) -> result::Result { - if value.is_zero() { - return Ok(NegativeImbalance::zero()) + /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce + /// `ExistentialDeposit` law, annulling the account as needed. + /// + /// It returns the result from the closure. Any dust is handled through the low-level + /// `fungible::Unbalanced` trap-door for legacy dust management. + /// + /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used + /// when it is known that the account already exists. + /// + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that + /// the caller will do this. + pub(crate) fn try_mutate_account_handling_dust>( + who: &T::AccountId, + f: impl FnOnce(&mut AccountData, bool) -> Result, + ) -> Result { + let (r, maybe_dust) = Self::try_mutate_account(who, f)?; + if let Some(dust) = maybe_dust { + >::handle_raw_dust(dust); + } + Ok(r) } - Self::try_mutate_account( - who, - |account, _| -> Result { - let new_free_account = - account.free.checked_sub(&value).ok_or(Error::::InsufficientBalance)?; - - // bail if we need to keep the account alive and this would kill it. - let ed = T::ExistentialDeposit::get(); - let would_be_dead = new_free_account + account.reserved < ed; - let would_kill = would_be_dead && account.free + account.reserved >= ed; - ensure!(liveness == AllowDeath || !would_kill, Error::::KeepAlive); - - Self::ensure_can_withdraw(who, value, reasons, new_free_account)?; - - account.free = new_free_account; + /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce + /// `ExistentialDeposit` law, annulling the account as needed. + /// + /// It returns both the result from the closure, and an optional amount of dust + /// which should be handled once it is known that all nested mutates that could affect + /// storage items what the dust handler touches have completed. + /// + /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used + /// when it is known that the account already exists. + /// + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that + /// the caller will do this. + pub(crate) fn mutate_account( + who: &T::AccountId, + f: impl FnOnce(&mut AccountData) -> R, + ) -> Result<(R, Option), DispatchError> { + Self::try_mutate_account(who, |a, _| -> Result { Ok(f(a)) }) + } + + /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce + /// `ExistentialDeposit` law, annulling the account as needed. This will do nothing if the + /// result of `f` is an `Err`. + /// + /// It returns both the result from the closure, and an optional amount of dust + /// which should be handled once it is known that all nested mutates that could affect + /// storage items what the dust handler touches have completed. + /// + /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used + /// when it is known that the account already exists. + /// + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that + /// the caller will do this. + pub(crate) fn try_mutate_account>( + who: &T::AccountId, + f: impl FnOnce(&mut AccountData, bool) -> Result, + ) -> Result<(R, Option), E> { + Self::ensure_upgraded(who); + let result = T::AccountStore::try_mutate_exists(who, |maybe_account| { + let is_new = maybe_account.is_none(); + let mut account = maybe_account.take().unwrap_or_default(); + let did_provide = account.free >= T::ExistentialDeposit::get(); + let did_consume = + !is_new && (!account.reserved.is_zero() || !account.frozen.is_zero()); + + let result = f(&mut account, is_new)?; + + let does_provide = account.free >= T::ExistentialDeposit::get(); + let does_consume = !account.reserved.is_zero() || !account.frozen.is_zero(); + + if !did_provide && does_provide { + frame_system::Pallet::::inc_providers(who); + } + if did_consume && !does_consume { + frame_system::Pallet::::dec_consumers(who); + } + if !did_consume && does_consume { + frame_system::Pallet::::inc_consumers(who)?; + } + if did_provide && !does_provide { + // This could reap the account so must go last. + frame_system::Pallet::::dec_providers(who).map_err(|r| { + if did_consume && !does_consume { + // best-effort revert consumer change. + let _ = frame_system::Pallet::::inc_consumers(who).defensive(); + } + if !did_consume && does_consume { + let _ = frame_system::Pallet::::dec_consumers(who); + } + r + })?; + } - Self::deposit_event(Event::Withdraw { who: who.clone(), amount: value }); - Ok(NegativeImbalance::new(value)) - }, - ) - } + let maybe_endowed = if is_new { Some(account.free) } else { None }; - /// Force the new free balance of a target account `who` to some new value `balance`. - fn make_free_balance_be( - who: &T::AccountId, - value: Self::Balance, - ) -> SignedImbalance { - Self::try_mutate_account( - who, - |account, - is_new| - -> Result, DispatchError> { + // Handle any steps needed after mutating an account. + // + // This includes DustRemoval unbalancing, in the case than the `new` account's total + // balance is non-zero but below ED. + // + // Updates `maybe_account` to `Some` iff the account has sufficient balance. + // Evaluates `maybe_dust`, which is `Some` containing the dust to be dropped, iff + // some dust should be dropped. + // + // We should never be dropping if reserved is non-zero. Reserved being non-zero + // should imply that we have a consumer ref, so this is economically safe. let ed = T::ExistentialDeposit::get(); - let total = value.saturating_add(account.reserved); - // If we're attempting to set an existing account to less than ED, then - // bypass the entire operation. It's a no-op if you follow it through, but - // since this is an instance where we might account for a negative imbalance - // (in the dust cleaner of set_account) before we account for its actual - // equal and opposite cause (returned as an Imbalance), then in the - // instance that there's no other accounts on the system at all, we might - // underflow the issuance and our arithmetic will be off. - ensure!(total >= ed || !is_new, Error::::ExistentialDeposit); - - let imbalance = if account.free <= value { - SignedImbalance::Positive(PositiveImbalance::new(value - account.free)) + let maybe_dust = if account.free < ed && account.reserved.is_zero() { + if account.free.is_zero() { + None + } else { + Some(account.free) + } } else { - SignedImbalance::Negative(NegativeImbalance::new(account.free - value)) - }; - account.free = value; - Self::deposit_event(Event::BalanceSet { - who: who.clone(), - free: account.free, - reserved: account.reserved, - }); - Ok(imbalance) - }, - ) - .unwrap_or_else(|_| SignedImbalance::Positive(Self::PositiveImbalance::zero())) - } -} - -impl, I: 'static> ReservableCurrency for Pallet -where - T::Balance: MaybeSerializeDeserialize + Debug, -{ - /// Check if `who` can reserve `value` from their free balance. - /// - /// Always `true` if value to be reserved is zero. - fn can_reserve(who: &T::AccountId, value: Self::Balance) -> bool { - if value.is_zero() { - return true - } - Self::account(who).free.checked_sub(&value).map_or(false, |new_balance| { - Self::ensure_can_withdraw(who, value, WithdrawReasons::RESERVE, new_balance).is_ok() - }) - } - - fn reserved_balance(who: &T::AccountId) -> Self::Balance { - Self::account(who).reserved - } - - /// Move `value` from the free balance from `who` to their reserved balance. - /// - /// Is a no-op if value to be reserved is zero. - fn reserve(who: &T::AccountId, value: Self::Balance) -> DispatchResult { - if value.is_zero() { - return Ok(()) - } - - Self::try_mutate_account(who, |account, _| -> DispatchResult { - account.free = - account.free.checked_sub(&value).ok_or(Error::::InsufficientBalance)?; - account.reserved = - account.reserved.checked_add(&value).ok_or(ArithmeticError::Overflow)?; - Self::ensure_can_withdraw(&who, value, WithdrawReasons::RESERVE, account.free) - })?; - - Self::deposit_event(Event::Reserved { who: who.clone(), amount: value }); - Ok(()) - } - - /// Unreserve some funds, returning any amount that was unable to be unreserved. - /// - /// Is a no-op if the value to be unreserved is zero or the account does not exist. - /// - /// NOTE: returns amount value which wasn't successfully unreserved. - fn unreserve(who: &T::AccountId, value: Self::Balance) -> Self::Balance { - if value.is_zero() { - return Zero::zero() - } - if Self::total_balance(who).is_zero() { - return value - } - - let actual = match Self::mutate_account(who, |account| { - let actual = cmp::min(account.reserved, value); - account.reserved -= actual; - // defensive only: this can never fail since total issuance which is at least - // free+reserved fits into the same data type. - account.free = account.free.defensive_saturating_add(actual); - actual - }) { - Ok(x) => x, - Err(_) => { - // This should never happen since we don't alter the total amount in the account. - // If it ever does, then we should fail gracefully though, indicating that nothing - // could be done. - return value - }, - }; - - Self::deposit_event(Event::Unreserved { who: who.clone(), amount: actual }); - value - actual - } - - /// Slash from reserved balance, returning the negative imbalance created, - /// and any amount that was unable to be slashed. - /// - /// Is a no-op if the value to be slashed is zero or the account does not exist. - fn slash_reserved( - who: &T::AccountId, - value: Self::Balance, - ) -> (Self::NegativeImbalance, Self::Balance) { - if value.is_zero() { - return (NegativeImbalance::zero(), Zero::zero()) - } - if Self::total_balance(who).is_zero() { - return (NegativeImbalance::zero(), value) - } - - // NOTE: `mutate_account` may fail if it attempts to reduce the balance to the point that an - // account is attempted to be illegally destroyed. - - for attempt in 0..2 { - match Self::mutate_account(who, |account| { - let best_value = match attempt { - 0 => value, - // If acting as a critical provider (i.e. first attempt failed), then ensure - // slash leaves at least the ED. - _ => value.min( - (account.free + account.reserved) - .saturating_sub(T::ExistentialDeposit::get()), - ), + assert!( + account.free.is_zero() || account.free >= ed || !account.reserved.is_zero() + ); + *maybe_account = Some(account); + None }; - - let actual = cmp::min(account.reserved, best_value); - account.reserved -= actual; - - // underflow should never happen, but it if does, there's nothing to be done here. - (NegativeImbalance::new(actual), value - actual) - }) { - Ok((imbalance, not_slashed)) => { - Self::deposit_event(Event::Slashed { - who: who.clone(), - amount: value.saturating_sub(not_slashed), + Ok((maybe_endowed, maybe_dust, result)) + }); + result.map(|(maybe_endowed, maybe_dust, result)| { + if let Some(endowed) = maybe_endowed { + Self::deposit_event(Event::Endowed { + account: who.clone(), + free_balance: endowed, }); - return (imbalance, not_slashed) - }, - Err(_) => (), - } - } - // Should never get here as we ensure that ED is left in the second attempt. - // In case we do, though, then we fail gracefully. - (Self::NegativeImbalance::zero(), value) - } - - /// Move the reserved balance of one account into the balance of another, according to `status`. - /// - /// Is a no-op if: - /// - the value to be moved is zero; or - /// - the `slashed` id equal to `beneficiary` and the `status` is `Reserved`. - fn repatriate_reserved( - slashed: &T::AccountId, - beneficiary: &T::AccountId, - value: Self::Balance, - status: Status, - ) -> Result { - let actual = Self::do_transfer_reserved(slashed, beneficiary, value, true, status)?; - Ok(value.saturating_sub(actual)) - } -} - -impl, I: 'static> NamedReservableCurrency for Pallet -where - T::Balance: MaybeSerializeDeserialize + Debug, -{ - type ReserveIdentifier = T::ReserveIdentifier; - - fn reserved_balance_named(id: &Self::ReserveIdentifier, who: &T::AccountId) -> Self::Balance { - let reserves = Self::reserves(who); - reserves - .binary_search_by_key(id, |data| data.id) - .map(|index| reserves[index].amount) - .unwrap_or_default() - } - - /// Move `value` from the free balance from `who` to a named reserve balance. - /// - /// Is a no-op if value to be reserved is zero. - fn reserve_named( - id: &Self::ReserveIdentifier, - who: &T::AccountId, - value: Self::Balance, - ) -> DispatchResult { - if value.is_zero() { - return Ok(()) - } - - Reserves::::try_mutate(who, |reserves| -> DispatchResult { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - // this add can't overflow but just to be defensive. - reserves[index].amount = reserves[index].amount.defensive_saturating_add(value); - }, - Err(index) => { - reserves - .try_insert(index, ReserveData { id: *id, amount: value }) - .map_err(|_| Error::::TooManyReserves)?; - }, - }; - >::reserve(who, value)?; - Ok(()) - }) - } - - /// Unreserve some funds, returning any amount that was unable to be unreserved. - /// - /// Is a no-op if the value to be unreserved is zero. - fn unreserve_named( - id: &Self::ReserveIdentifier, - who: &T::AccountId, - value: Self::Balance, - ) -> Self::Balance { - if value.is_zero() { - return Zero::zero() + } + if let Some(amount) = maybe_dust { + Pallet::::deposit_event(Event::DustLost { account: who.clone(), amount }); + } + (result, maybe_dust) + }) } - Reserves::::mutate_exists(who, |maybe_reserves| -> Self::Balance { - if let Some(reserves) = maybe_reserves.as_mut() { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - let to_change = cmp::min(reserves[index].amount, value); - - let remain = >::unreserve(who, to_change); - - // remain should always be zero but just to be defensive here. - let actual = to_change.defensive_saturating_sub(remain); - - // `actual <= to_change` and `to_change <= amount`; qed; - reserves[index].amount -= actual; + /// Update the account entry for `who`, given the locks. + pub(crate) fn update_locks(who: &T::AccountId, locks: &[BalanceLock]) { + let bounded_locks = WeakBoundedVec::<_, T::MaxLocks>::force_from( + locks.to_vec(), + Some("Balances Update Locks"), + ); - if reserves[index].amount.is_zero() { - if reserves.len() == 1 { - // no more named reserves - *maybe_reserves = None; - } else { - // remove this named reserve - reserves.remove(index); - } - } + if locks.len() as u32 > T::MaxLocks::get() { + log::warn!( + target: LOG_TARGET, + "Warning: A user has more currency locks than expected. \ + A runtime configuration adjustment may be needed." + ); + } + let freezes = Freezes::::get(who); + // TODO: Revisit this assumption. We no manipulate consumer/provider refs. + // No way this can fail since we do not alter the existential balances. + let res = Self::mutate_account(who, |b| { + b.frozen = Zero::zero(); + for l in locks.iter() { + b.frozen = b.frozen.max(l.amount); + } + for l in freezes.iter() { + b.frozen = b.frozen.max(l.amount); + } + }); + debug_assert!(res.is_ok()); + if let Ok((_, maybe_dust)) = res { + debug_assert!(maybe_dust.is_none(), "Not altering main balance; qed"); + } - value - actual - }, - Err(_) => value, + let existed = Locks::::contains_key(who); + if locks.is_empty() { + Locks::::remove(who); + if existed { + // TODO: use Locks::::hashed_key + // https://github.com/paritytech/substrate/issues/4969 + system::Pallet::::dec_consumers(who); } } else { - value + Locks::::insert(who, bounded_locks); + if !existed && system::Pallet::::inc_consumers_without_limit(who).is_err() { + // No providers for the locks. This is impossible under normal circumstances + // since the funds that are under the lock will themselves be stored in the + // account and therefore will need a reference. + log::warn!( + target: LOG_TARGET, + "Warning: Attempt to introduce lock consumer reference, yet no providers. \ + This is unexpected but should be safe." + ); + } } - }) - } - - /// Slash from reserved balance, returning the negative imbalance created, - /// and any amount that was unable to be slashed. - /// - /// Is a no-op if the value to be slashed is zero. - fn slash_reserved_named( - id: &Self::ReserveIdentifier, - who: &T::AccountId, - value: Self::Balance, - ) -> (Self::NegativeImbalance, Self::Balance) { - if value.is_zero() { - return (NegativeImbalance::zero(), Zero::zero()) } - Reserves::::mutate(who, |reserves| -> (Self::NegativeImbalance, Self::Balance) { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - let to_change = cmp::min(reserves[index].amount, value); - - let (imb, remain) = - >::slash_reserved(who, to_change); - - // remain should always be zero but just to be defensive here. - let actual = to_change.defensive_saturating_sub(remain); - - // `actual <= to_change` and `to_change <= amount`; qed; - reserves[index].amount -= actual; - - Self::deposit_event(Event::Slashed { who: who.clone(), amount: actual }); - (imb, value - actual) - }, - Err(_) => (NegativeImbalance::zero(), value), + /// Update the account entry for `who`, given the locks. + pub(crate) fn update_freezes( + who: &T::AccountId, + freezes: BoundedSlice, T::MaxFreezes>, + ) -> DispatchResult { + let (_, maybe_dust) = Self::mutate_account(who, |b| { + b.frozen = Zero::zero(); + for l in Locks::::get(who).iter() { + b.frozen = b.frozen.max(l.amount); + } + for l in freezes.iter() { + b.frozen = b.frozen.max(l.amount); + } + })?; + debug_assert!(maybe_dust.is_none(), "Not altering main balance; qed"); + if freezes.is_empty() { + Freezes::::remove(who); + } else { + Freezes::::insert(who, freezes); } - }) - } - - /// Move the reserved balance of one account into the balance of another, according to `status`. - /// If `status` is `Reserved`, the balance will be reserved with given `id`. - /// - /// Is a no-op if: - /// - the value to be moved is zero; or - /// - the `slashed` id equal to `beneficiary` and the `status` is `Reserved`. - fn repatriate_reserved_named( - id: &Self::ReserveIdentifier, - slashed: &T::AccountId, - beneficiary: &T::AccountId, - value: Self::Balance, - status: Status, - ) -> Result { - if value.is_zero() { - return Ok(Zero::zero()) + Ok(()) } - if slashed == beneficiary { - return match status { - Status::Free => Ok(Self::unreserve_named(id, slashed, value)), - Status::Reserved => - Ok(value.saturating_sub(Self::reserved_balance_named(id, slashed))), + /// Move the reserved balance of one account into the balance of another, according to + /// `status`. + /// + /// Is a no-op if: + /// - the value to be moved is zero; or + /// - the `slashed` id equal to `beneficiary` and the `status` is `Reserved`. + /// + /// NOTE: returns actual amount of transferred value in `Ok` case. + pub(crate) fn do_transfer_reserved( + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: T::Balance, + best_effort: bool, + status: Status, + ) -> Result { + if value.is_zero() { + return Ok(Zero::zero()) } - } - - Reserves::::try_mutate(slashed, |reserves| -> Result { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - let to_change = cmp::min(reserves[index].amount, value); - - let actual = if status == Status::Reserved { - // make it the reserved under same identifier - Reserves::::try_mutate( - beneficiary, - |reserves| -> Result { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - let remain = - >::repatriate_reserved( - slashed, - beneficiary, - to_change, - status, - )?; - - // remain should always be zero but just to be defensive - // here. - let actual = to_change.defensive_saturating_sub(remain); - - // this add can't overflow but just to be defensive. - reserves[index].amount = - reserves[index].amount.defensive_saturating_add(actual); - - Ok(actual) - }, - Err(index) => { - let remain = - >::repatriate_reserved( - slashed, - beneficiary, - to_change, - status, - )?; - - // remain should always be zero but just to be defensive - // here - let actual = to_change.defensive_saturating_sub(remain); - - reserves - .try_insert( - index, - ReserveData { id: *id, amount: actual }, - ) - .map_err(|_| Error::::TooManyReserves)?; - - Ok(actual) - }, - } - }, - )? - } else { - let remain = >::repatriate_reserved( - slashed, - beneficiary, - to_change, - status, - )?; - - // remain should always be zero but just to be defensive here - to_change.defensive_saturating_sub(remain) - }; - // `actual <= to_change` and `to_change <= amount`; qed; - reserves[index].amount -= actual; + if slashed == beneficiary { + return match status { + Status::Free => Ok(value.saturating_sub(Self::unreserve(slashed, value))), + Status::Reserved => Ok(value.saturating_sub(Self::reserved_balance(slashed))), + } + } - Ok(value - actual) + let ((actual, maybe_dust_1), maybe_dust_2) = Self::try_mutate_account( + beneficiary, + |to_account, is_new| -> Result<(T::Balance, Option), DispatchError> { + ensure!(!is_new, Error::::DeadAccount); + Self::try_mutate_account( + slashed, + |from_account, _| -> Result { + let actual = cmp::min(from_account.reserved, value); + ensure!( + best_effort || actual == value, + Error::::InsufficientBalance + ); + match status { + Status::Free => + to_account.free = to_account + .free + .checked_add(&actual) + .ok_or(ArithmeticError::Overflow)?, + Status::Reserved => + to_account.reserved = to_account + .reserved + .checked_add(&actual) + .ok_or(ArithmeticError::Overflow)?, + } + from_account.reserved -= actual; + Ok(actual) + }, + ) }, - Err(_) => Ok(value), - } - }) - } -} + )?; -impl, I: 'static> LockableCurrency for Pallet -where - T::Balance: MaybeSerializeDeserialize + Debug, -{ - type Moment = T::BlockNumber; - - type MaxLocks = T::MaxLocks; - - // Set a lock on the balance of `who`. - // Is a no-op if lock amount is zero or `reasons` `is_none()`. - fn set_lock( - id: LockIdentifier, - who: &T::AccountId, - amount: T::Balance, - reasons: WithdrawReasons, - ) { - if amount.is_zero() || reasons.is_empty() { - return - } - let mut new_lock = Some(BalanceLock { id, amount, reasons: reasons.into() }); - let mut locks = Self::locks(who) - .into_iter() - .filter_map(|l| if l.id == id { new_lock.take() } else { Some(l) }) - .collect::>(); - if let Some(lock) = new_lock { - locks.push(lock) - } - Self::update_locks(who, &locks[..]); - } + if let Some(dust) = maybe_dust_1 { + >::handle_raw_dust(dust); + } + if let Some(dust) = maybe_dust_2 { + >::handle_raw_dust(dust); + } - // Extend a lock on the balance of `who`. - // Is a no-op if lock amount is zero or `reasons` `is_none()`. - fn extend_lock( - id: LockIdentifier, - who: &T::AccountId, - amount: T::Balance, - reasons: WithdrawReasons, - ) { - if amount.is_zero() || reasons.is_empty() { - return - } - let mut new_lock = Some(BalanceLock { id, amount, reasons: reasons.into() }); - let mut locks = Self::locks(who) - .into_iter() - .filter_map(|l| { - if l.id == id { - new_lock.take().map(|nl| BalanceLock { - id: l.id, - amount: l.amount.max(nl.amount), - reasons: l.reasons | nl.reasons, - }) - } else { - Some(l) - } - }) - .collect::>(); - if let Some(lock) = new_lock { - locks.push(lock) + Self::deposit_event(Event::ReserveRepatriated { + from: slashed.clone(), + to: beneficiary.clone(), + amount: actual, + destination_status: status, + }); + Ok(actual) } - Self::update_locks(who, &locks[..]); - } - - fn remove_lock(id: LockIdentifier, who: &T::AccountId) { - let mut locks = Self::locks(who); - locks.retain(|l| l.id != id); - Self::update_locks(who, &locks[..]); } } diff --git a/frame/balances/src/tests.rs b/frame/balances/src/tests.rs deleted file mode 100644 index b4233a6c3a..0000000000 --- a/frame/balances/src/tests.rs +++ /dev/null @@ -1,1460 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Macro for creating the tests for the module. - -#![cfg(test)] - -#[macro_export] -macro_rules! decl_tests { - ($test:ty, $ext_builder:ty, $existential_deposit:expr) => { - - use crate::*; - use sp_runtime::{ArithmeticError, TokenError, FixedPointNumber, traits::{SignedExtension, BadOrigin}}; - use frame_support::{ - assert_noop, assert_storage_noop, assert_ok, assert_err, - traits::{ - LockableCurrency, LockIdentifier, WithdrawReasons, - Currency, ReservableCurrency, ExistenceRequirement::AllowDeath - } - }; - use pallet_transaction_payment::{ChargeTransactionPayment, Multiplier}; - use frame_system::RawOrigin; - - const ID_1: LockIdentifier = *b"1 "; - const ID_2: LockIdentifier = *b"2 "; - - pub const CALL: &<$test as frame_system::Config>::RuntimeCall = - &RuntimeCall::Balances(pallet_balances::Call::transfer { dest: 0, value: 0 }); - - /// create a transaction info struct from weight. Handy to avoid building the whole struct. - pub fn info_from_weight(w: Weight) -> DispatchInfo { - DispatchInfo { weight: w, ..Default::default() } - } - - fn events() -> Vec { - let evt = System::events().into_iter().map(|evt| evt.event).collect::>(); - - System::reset_events(); - - evt - } - - #[test] - fn basic_locking_should_work() { - <$ext_builder>::default().existential_deposit(1).monied(true).build().execute_with(|| { - assert_eq!(Balances::free_balance(1), 10); - Balances::set_lock(ID_1, &1, 9, WithdrawReasons::all()); - assert_noop!( - >::transfer(&1, &2, 5, AllowDeath), - Error::<$test, _>::LiquidityRestrictions - ); - }); - } - - #[test] - fn account_should_be_reaped() { - <$ext_builder>::default().existential_deposit(1).monied(true).build().execute_with(|| { - assert_eq!(Balances::free_balance(1), 10); - assert_ok!(>::transfer(&1, &2, 10, AllowDeath)); - // Check that the account is dead. - assert!(!frame_system::Account::::contains_key(&1)); - }); - } - - #[test] - fn reap_failed_due_to_provider_and_consumer() { - <$ext_builder>::default().existential_deposit(1).monied(true).build().execute_with(|| { - // SCENARIO: only one provider and there are remaining consumers. - assert_ok!(System::inc_consumers(&1)); - assert!(!System::can_dec_provider(&1)); - assert_noop!( - >::transfer(&1, &2, 10, AllowDeath), - Error::<$test, _>::KeepAlive - ); - assert!(System::account_exists(&1)); - assert_eq!(Balances::free_balance(1), 10); - - // SCENARIO: more than one provider, but will not kill account due to other provider. - assert_eq!(System::inc_providers(&1), frame_system::IncRefStatus::Existed); - assert_eq!(System::providers(&1), 2); - assert!(System::can_dec_provider(&1)); - assert_ok!(>::transfer(&1, &2, 10, AllowDeath)); - assert_eq!(System::providers(&1), 1); - assert!(System::account_exists(&1)); - assert_eq!(Balances::free_balance(1), 0); - }); - } - - #[test] - fn partial_locking_should_work() { - <$ext_builder>::default().existential_deposit(1).monied(true).build().execute_with(|| { - Balances::set_lock(ID_1, &1, 5, WithdrawReasons::all()); - assert_ok!(>::transfer(&1, &2, 1, AllowDeath)); - }); - } - - #[test] - fn lock_removal_should_work() { - <$ext_builder>::default().existential_deposit(1).monied(true).build().execute_with(|| { - Balances::set_lock(ID_1, &1, u64::MAX, WithdrawReasons::all()); - Balances::remove_lock(ID_1, &1); - assert_ok!(>::transfer(&1, &2, 1, AllowDeath)); - }); - } - - #[test] - fn lock_replacement_should_work() { - <$ext_builder>::default().existential_deposit(1).monied(true).build().execute_with(|| { - Balances::set_lock(ID_1, &1, u64::MAX, WithdrawReasons::all()); - Balances::set_lock(ID_1, &1, 5, WithdrawReasons::all()); - assert_ok!(>::transfer(&1, &2, 1, AllowDeath)); - }); - } - - #[test] - fn double_locking_should_work() { - <$ext_builder>::default().existential_deposit(1).monied(true).build().execute_with(|| { - Balances::set_lock(ID_1, &1, 5, WithdrawReasons::all()); - Balances::set_lock(ID_2, &1, 5, WithdrawReasons::all()); - assert_ok!(>::transfer(&1, &2, 1, AllowDeath)); - }); - } - - #[test] - fn combination_locking_should_work() { - <$ext_builder>::default().existential_deposit(1).monied(true).build().execute_with(|| { - Balances::set_lock(ID_1, &1, u64::MAX, WithdrawReasons::empty()); - Balances::set_lock(ID_2, &1, 0, WithdrawReasons::all()); - assert_ok!(>::transfer(&1, &2, 1, AllowDeath)); - }); - } - - #[test] - fn lock_value_extension_should_work() { - <$ext_builder>::default().existential_deposit(1).monied(true).build().execute_with(|| { - Balances::set_lock(ID_1, &1, 5, WithdrawReasons::all()); - assert_noop!( - >::transfer(&1, &2, 6, AllowDeath), - Error::<$test, _>::LiquidityRestrictions - ); - Balances::extend_lock(ID_1, &1, 2, WithdrawReasons::all()); - assert_noop!( - >::transfer(&1, &2, 6, AllowDeath), - Error::<$test, _>::LiquidityRestrictions - ); - Balances::extend_lock(ID_1, &1, 8, WithdrawReasons::all()); - assert_noop!( - >::transfer(&1, &2, 3, AllowDeath), - Error::<$test, _>::LiquidityRestrictions - ); - }); - } - - #[test] - fn lock_reasons_should_work() { - <$ext_builder>::default() - .existential_deposit(1) - .monied(true) - .build() - .execute_with(|| { - pallet_transaction_payment::NextFeeMultiplier::<$test>::put( - Multiplier::saturating_from_integer(1) - ); - Balances::set_lock(ID_1, &1, 10, WithdrawReasons::RESERVE); - assert_noop!( - >::transfer(&1, &2, 1, AllowDeath), - Error::<$test, _>::LiquidityRestrictions - ); - assert_noop!( - >::reserve(&1, 1), - Error::<$test, _>::LiquidityRestrictions, - ); - assert!( as SignedExtension>::pre_dispatch( - ChargeTransactionPayment::from(1), - &1, - CALL, - &info_from_weight(Weight::from_parts(1, 0)), - 1, - ).is_err()); - assert_ok!( as SignedExtension>::pre_dispatch( - ChargeTransactionPayment::from(0), - &1, - CALL, - &info_from_weight(Weight::from_parts(1, 0)), - 1, - )); - - Balances::set_lock(ID_1, &1, 10, WithdrawReasons::TRANSACTION_PAYMENT); - assert_ok!(>::transfer(&1, &2, 1, AllowDeath)); - assert_ok!(>::reserve(&1, 1)); - assert!( as SignedExtension>::pre_dispatch( - ChargeTransactionPayment::from(1), - &1, - CALL, - &info_from_weight(Weight::from_parts(1, 0)), - 1, - ).is_err()); - assert!( as SignedExtension>::pre_dispatch( - ChargeTransactionPayment::from(0), - &1, - CALL, - &info_from_weight(Weight::from_parts(1, 0)), - 1, - ).is_err()); - }); - } - - #[test] - fn lock_block_number_extension_should_work() { - <$ext_builder>::default().existential_deposit(1).monied(true).build().execute_with(|| { - Balances::set_lock(ID_1, &1, 10, WithdrawReasons::all()); - assert_noop!( - >::transfer(&1, &2, 6, AllowDeath), - Error::<$test, _>::LiquidityRestrictions - ); - Balances::extend_lock(ID_1, &1, 10, WithdrawReasons::all()); - assert_noop!( - >::transfer(&1, &2, 6, AllowDeath), - Error::<$test, _>::LiquidityRestrictions - ); - System::set_block_number(2); - Balances::extend_lock(ID_1, &1, 10, WithdrawReasons::all()); - assert_noop!( - >::transfer(&1, &2, 3, AllowDeath), - Error::<$test, _>::LiquidityRestrictions - ); - }); - } - - #[test] - fn lock_reasons_extension_should_work() { - <$ext_builder>::default().existential_deposit(1).monied(true).build().execute_with(|| { - Balances::set_lock(ID_1, &1, 10, WithdrawReasons::TRANSFER); - assert_noop!( - >::transfer(&1, &2, 6, AllowDeath), - Error::<$test, _>::LiquidityRestrictions - ); - Balances::extend_lock(ID_1, &1, 10, WithdrawReasons::empty()); - assert_noop!( - >::transfer(&1, &2, 6, AllowDeath), - Error::<$test, _>::LiquidityRestrictions - ); - Balances::extend_lock(ID_1, &1, 10, WithdrawReasons::RESERVE); - assert_noop!( - >::transfer(&1, &2, 6, AllowDeath), - Error::<$test, _>::LiquidityRestrictions - ); - }); - } - - #[test] - fn default_indexing_on_new_accounts_should_not_work2() { - <$ext_builder>::default() - .existential_deposit(10) - .monied(true) - .build() - .execute_with(|| { - // account 5 should not exist - // ext_deposit is 10, value is 9, not satisfies for ext_deposit - assert_noop!( - Balances::transfer(Some(1).into(), 5, 9), - Error::<$test, _>::ExistentialDeposit, - ); - assert_eq!(Balances::free_balance(1), 100); - }); - } - - #[test] - fn reserved_balance_should_prevent_reclaim_count() { - <$ext_builder>::default() - .existential_deposit(256 * 1) - .monied(true) - .build() - .execute_with(|| { - System::inc_account_nonce(&2); - assert_eq!(Balances::total_balance(&2), 256 * 20); - - assert_ok!(Balances::reserve(&2, 256 * 19 + 1)); // account 2 becomes mostly reserved - assert_eq!(Balances::free_balance(2), 255); // "free" account deleted." - assert_eq!(Balances::total_balance(&2), 256 * 20); // reserve still exists. - assert_eq!(System::account_nonce(&2), 1); - - // account 4 tries to take index 1 for account 5. - assert_ok!(Balances::transfer(Some(4).into(), 5, 256 * 1 + 0x69)); - assert_eq!(Balances::total_balance(&5), 256 * 1 + 0x69); - - assert!(Balances::slash(&2, 256 * 19 + 2).1.is_zero()); // account 2 gets slashed - // "reserve" account reduced to 255 (below ED) so account deleted - assert_eq!(Balances::total_balance(&2), 0); - assert_eq!(System::account_nonce(&2), 0); // nonce zero - - // account 4 tries to take index 1 again for account 6. - assert_ok!(Balances::transfer(Some(4).into(), 6, 256 * 1 + 0x69)); - assert_eq!(Balances::total_balance(&6), 256 * 1 + 0x69); - }); - } - - #[test] - fn reward_should_work() { - <$ext_builder>::default().monied(true).build().execute_with(|| { - assert_eq!(Balances::total_balance(&1), 10); - assert_ok!(Balances::deposit_into_existing(&1, 10).map(drop)); - System::assert_last_event(RuntimeEvent::Balances(crate::Event::Deposit { who: 1, amount: 10 })); - assert_eq!(Balances::total_balance(&1), 20); - assert_eq!(>::get(), 120); - }); - } - - #[test] - fn dust_account_removal_should_work() { - <$ext_builder>::default() - .existential_deposit(100) - .monied(true) - .build() - .execute_with(|| { - System::inc_account_nonce(&2); - assert_eq!(System::account_nonce(&2), 1); - assert_eq!(Balances::total_balance(&2), 2000); - // index 1 (account 2) becomes zombie - assert_ok!(Balances::transfer(Some(2).into(), 5, 1901)); - assert_eq!(Balances::total_balance(&2), 0); - assert_eq!(Balances::total_balance(&5), 1901); - assert_eq!(System::account_nonce(&2), 0); - }); - } - - #[test] - fn balance_works() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 42); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit { who: 1, amount: 42 })); - assert_eq!(Balances::free_balance(1), 42); - assert_eq!(Balances::reserved_balance(1), 0); - assert_eq!(Balances::total_balance(&1), 42); - assert_eq!(Balances::free_balance(2), 0); - assert_eq!(Balances::reserved_balance(2), 0); - assert_eq!(Balances::total_balance(&2), 0); - }); - } - - #[test] - fn balance_transfer_works() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 111); - assert_ok!(Balances::transfer(Some(1).into(), 2, 69)); - assert_eq!(Balances::total_balance(&1), 42); - assert_eq!(Balances::total_balance(&2), 69); - }); - } - - #[test] - fn force_transfer_works() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 111); - assert_noop!( - Balances::force_transfer(Some(2).into(), 1, 2, 69), - BadOrigin, - ); - assert_ok!(Balances::force_transfer(RawOrigin::Root.into(), 1, 2, 69)); - assert_eq!(Balances::total_balance(&1), 42); - assert_eq!(Balances::total_balance(&2), 69); - }); - } - - #[test] - fn reserving_balance_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 111); - - assert_eq!(Balances::total_balance(&1), 111); - assert_eq!(Balances::free_balance(1), 111); - assert_eq!(Balances::reserved_balance(1), 0); - - assert_ok!(Balances::reserve(&1, 69)); - - assert_eq!(Balances::total_balance(&1), 111); - assert_eq!(Balances::free_balance(1), 42); - assert_eq!(Balances::reserved_balance(1), 69); - }); - } - - #[test] - fn balance_transfer_when_reserved_should_not_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 111); - assert_ok!(Balances::reserve(&1, 69)); - assert_noop!( - Balances::transfer(Some(1).into(), 2, 69), - Error::<$test, _>::InsufficientBalance, - ); - }); - } - - #[test] - fn deducting_balance_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 111); - assert_ok!(Balances::reserve(&1, 69)); - assert_eq!(Balances::free_balance(1), 42); - }); - } - - #[test] - fn refunding_balance_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 42); - assert_ok!(Balances::mutate_account(&1, |a| a.reserved = 69)); - Balances::unreserve(&1, 69); - assert_eq!(Balances::free_balance(1), 111); - assert_eq!(Balances::reserved_balance(1), 0); - }); - } - - #[test] - fn slashing_balance_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 111); - assert_ok!(Balances::reserve(&1, 69)); - assert!(Balances::slash(&1, 69).1.is_zero()); - assert_eq!(Balances::free_balance(1), 0); - assert_eq!(Balances::reserved_balance(1), 42); - assert_eq!(>::get(), 42); - }); - } - - #[test] - fn withdrawing_balance_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&2, 111); - let _ = Balances::withdraw( - &2, 11, WithdrawReasons::TRANSFER, ExistenceRequirement::KeepAlive - ); - System::assert_last_event(RuntimeEvent::Balances(crate::Event::Withdraw { who: 2, amount: 11 })); - assert_eq!(Balances::free_balance(2), 100); - assert_eq!(>::get(), 100); - }); - } - - #[test] - fn slashing_incomplete_balance_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 42); - assert_ok!(Balances::reserve(&1, 21)); - assert_eq!(Balances::slash(&1, 69).1, 27); - assert_eq!(Balances::free_balance(1), 0); - assert_eq!(Balances::reserved_balance(1), 0); - assert_eq!(>::get(), 0); - }); - } - - #[test] - fn unreserving_balance_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 111); - assert_ok!(Balances::reserve(&1, 111)); - Balances::unreserve(&1, 42); - assert_eq!(Balances::reserved_balance(1), 69); - assert_eq!(Balances::free_balance(1), 42); - }); - } - - #[test] - fn slashing_reserved_balance_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 111); - assert_ok!(Balances::reserve(&1, 111)); - assert_eq!(Balances::slash_reserved(&1, 42).1, 0); - assert_eq!(Balances::reserved_balance(1), 69); - assert_eq!(Balances::free_balance(1), 0); - assert_eq!(>::get(), 69); - }); - } - - #[test] - fn slashing_incomplete_reserved_balance_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 111); - assert_ok!(Balances::reserve(&1, 42)); - assert_eq!(Balances::slash_reserved(&1, 69).1, 27); - assert_eq!(Balances::free_balance(1), 69); - assert_eq!(Balances::reserved_balance(1), 0); - assert_eq!(>::get(), 69); - }); - } - - #[test] - fn repatriating_reserved_balance_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 110); - let _ = Balances::deposit_creating(&2, 1); - assert_ok!(Balances::reserve(&1, 110)); - assert_ok!(Balances::repatriate_reserved(&1, &2, 41, Status::Free), 0); - System::assert_last_event( - RuntimeEvent::Balances(crate::Event::ReserveRepatriated { from: 1, to: 2, amount: 41, destination_status: Status::Free }) - ); - assert_eq!(Balances::reserved_balance(1), 69); - assert_eq!(Balances::free_balance(1), 0); - assert_eq!(Balances::reserved_balance(2), 0); - assert_eq!(Balances::free_balance(2), 42); - }); - } - - #[test] - fn transferring_reserved_balance_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 110); - let _ = Balances::deposit_creating(&2, 1); - assert_ok!(Balances::reserve(&1, 110)); - assert_ok!(Balances::repatriate_reserved(&1, &2, 41, Status::Reserved), 0); - assert_eq!(Balances::reserved_balance(1), 69); - assert_eq!(Balances::free_balance(1), 0); - assert_eq!(Balances::reserved_balance(2), 41); - assert_eq!(Balances::free_balance(2), 1); - }); - } - - #[test] - fn transferring_reserved_balance_to_yourself_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 110); - assert_ok!(Balances::reserve(&1, 50)); - assert_ok!(Balances::repatriate_reserved(&1, &1, 50, Status::Free), 0); - assert_eq!(Balances::free_balance(1), 110); - assert_eq!(Balances::reserved_balance(1), 0); - - assert_ok!(Balances::reserve(&1, 50)); - assert_ok!(Balances::repatriate_reserved(&1, &1, 60, Status::Free), 10); - assert_eq!(Balances::free_balance(1), 110); - assert_eq!(Balances::reserved_balance(1), 0); - }); - } - - #[test] - fn transferring_reserved_balance_to_nonexistent_should_fail() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 111); - assert_ok!(Balances::reserve(&1, 111)); - assert_noop!(Balances::repatriate_reserved(&1, &2, 42, Status::Free), Error::<$test, _>::DeadAccount); - }); - } - - #[test] - fn transferring_incomplete_reserved_balance_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 110); - let _ = Balances::deposit_creating(&2, 1); - assert_ok!(Balances::reserve(&1, 41)); - assert_ok!(Balances::repatriate_reserved(&1, &2, 69, Status::Free), 28); - assert_eq!(Balances::reserved_balance(1), 0); - assert_eq!(Balances::free_balance(1), 69); - assert_eq!(Balances::reserved_balance(2), 0); - assert_eq!(Balances::free_balance(2), 42); - }); - } - - #[test] - fn transferring_too_high_value_should_not_panic() { - <$ext_builder>::default().build().execute_with(|| { - Balances::make_free_balance_be(&1, u64::MAX); - Balances::make_free_balance_be(&2, 1); - - assert_err!( - Balances::transfer(Some(1).into(), 2, u64::MAX), - ArithmeticError::Overflow, - ); - - assert_eq!(Balances::free_balance(1), u64::MAX); - assert_eq!(Balances::free_balance(2), 1); - }); - } - - #[test] - fn account_create_on_free_too_low_with_other() { - <$ext_builder>::default().existential_deposit(100).build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 100); - assert_eq!(>::get(), 100); - - // No-op. - let _ = Balances::deposit_creating(&2, 50); - assert_eq!(Balances::free_balance(2), 0); - assert_eq!(>::get(), 100); - }) - } - - #[test] - fn account_create_on_free_too_low() { - <$ext_builder>::default().existential_deposit(100).build().execute_with(|| { - // No-op. - let _ = Balances::deposit_creating(&2, 50); - assert_eq!(Balances::free_balance(2), 0); - assert_eq!(>::get(), 0); - }) - } - - #[test] - fn account_removal_on_free_too_low() { - <$ext_builder>::default().existential_deposit(100).build().execute_with(|| { - assert_eq!(>::get(), 0); - - // Setup two accounts with free balance above the existential threshold. - let _ = Balances::deposit_creating(&1, 110); - let _ = Balances::deposit_creating(&2, 110); - - assert_eq!(Balances::free_balance(1), 110); - assert_eq!(Balances::free_balance(2), 110); - assert_eq!(>::get(), 220); - - // Transfer funds from account 1 of such amount that after this transfer - // the balance of account 1 will be below the existential threshold. - // This should lead to the removal of all balance of this account. - assert_ok!(Balances::transfer(Some(1).into(), 2, 20)); - - // Verify free balance removal of account 1. - assert_eq!(Balances::free_balance(1), 0); - assert_eq!(Balances::free_balance(2), 130); - - // Verify that TotalIssuance tracks balance removal when free balance is too low. - assert_eq!(>::get(), 130); - }); - } - - #[test] - fn burn_must_work() { - <$ext_builder>::default().monied(true).build().execute_with(|| { - let init_total_issuance = Balances::total_issuance(); - let imbalance = Balances::burn(10); - assert_eq!(Balances::total_issuance(), init_total_issuance - 10); - drop(imbalance); - assert_eq!(Balances::total_issuance(), init_total_issuance); - }); - } - - #[test] - fn transfer_keep_alive_works() { - <$ext_builder>::default().existential_deposit(1).build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 100); - assert_noop!( - Balances::transfer_keep_alive(Some(1).into(), 2, 100), - Error::<$test, _>::KeepAlive - ); - assert_eq!(Balances::total_balance(&1), 100); - assert_eq!(Balances::total_balance(&2), 0); - }); - } - - #[test] - #[should_panic = "the balance of any account should always be at least the existential deposit."] - fn cannot_set_genesis_value_below_ed() { - ($existential_deposit).with(|v| *v.borrow_mut() = 11); - let mut t = frame_system::GenesisConfig::default().build_storage::<$test>().unwrap(); - let _ = pallet_balances::GenesisConfig::<$test> { - balances: vec![(1, 10)], - }.assimilate_storage(&mut t).unwrap(); - } - - #[test] - #[should_panic = "duplicate balances in genesis."] - fn cannot_set_genesis_value_twice() { - let mut t = frame_system::GenesisConfig::default().build_storage::<$test>().unwrap(); - let _ = pallet_balances::GenesisConfig::<$test> { - balances: vec![(1, 10), (2, 20), (1, 15)], - }.assimilate_storage(&mut t).unwrap(); - } - - #[test] - fn dust_moves_between_free_and_reserved() { - <$ext_builder>::default() - .existential_deposit(100) - .build() - .execute_with(|| { - // Set balance to free and reserved at the existential deposit - assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 1, 100, 0)); - // Check balance - assert_eq!(Balances::free_balance(1), 100); - assert_eq!(Balances::reserved_balance(1), 0); - - // Reserve some free balance - assert_ok!(Balances::reserve(&1, 50)); - // Check balance, the account should be ok. - assert_eq!(Balances::free_balance(1), 50); - assert_eq!(Balances::reserved_balance(1), 50); - - // Reserve the rest of the free balance - assert_ok!(Balances::reserve(&1, 50)); - // Check balance, the account should be ok. - assert_eq!(Balances::free_balance(1), 0); - assert_eq!(Balances::reserved_balance(1), 100); - - // Unreserve everything - Balances::unreserve(&1, 100); - // Check balance, all 100 should move to free_balance - assert_eq!(Balances::free_balance(1), 100); - assert_eq!(Balances::reserved_balance(1), 0); - }); - } - - #[test] - fn account_deleted_when_just_dust() { - <$ext_builder>::default() - .existential_deposit(100) - .build() - .execute_with(|| { - // Set balance to free and reserved at the existential deposit - assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 1, 50, 50)); - // Check balance - assert_eq!(Balances::free_balance(1), 50); - assert_eq!(Balances::reserved_balance(1), 50); - - // Reserve some free balance - let res = Balances::slash(&1, 1); - assert_eq!(res, (NegativeImbalance::new(1), 0)); - - // The account should be dead. - assert_eq!(Balances::free_balance(1), 0); - assert_eq!(Balances::reserved_balance(1), 0); - }); - } - - #[test] - fn emit_events_with_reserve_and_unreserve() { - <$ext_builder>::default() - .build() - .execute_with(|| { - let _ = Balances::deposit_creating(&1, 100); - - System::set_block_number(2); - assert_ok!(Balances::reserve(&1, 10)); - - System::assert_last_event(RuntimeEvent::Balances(crate::Event::Reserved { who: 1, amount: 10 })); - - System::set_block_number(3); - assert!(Balances::unreserve(&1, 5).is_zero()); - - System::assert_last_event(RuntimeEvent::Balances(crate::Event::Unreserved { who: 1, amount: 5 })); - - System::set_block_number(4); - assert_eq!(Balances::unreserve(&1, 6), 1); - - // should only unreserve 5 - System::assert_last_event(RuntimeEvent::Balances(crate::Event::Unreserved { who: 1, amount: 5 })); - }); - } - - #[test] - fn emit_events_with_existential_deposit() { - <$ext_builder>::default() - .existential_deposit(100) - .build() - .execute_with(|| { - assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 1, 100, 0)); - - assert_eq!( - events(), - [ - RuntimeEvent::System(system::Event::NewAccount { account: 1 }), - RuntimeEvent::Balances(crate::Event::Endowed { account: 1, free_balance: 100 }), - RuntimeEvent::Balances(crate::Event::BalanceSet { who: 1, free: 100, reserved: 0 }), - ] - ); - - let res = Balances::slash(&1, 1); - assert_eq!(res, (NegativeImbalance::new(1), 0)); - - assert_eq!( - events(), - [ - RuntimeEvent::System(system::Event::KilledAccount { account: 1 }), - RuntimeEvent::Balances(crate::Event::DustLost { account: 1, amount: 99 }), - RuntimeEvent::Balances(crate::Event::Slashed { who: 1, amount: 1 }), - ] - ); - }); - } - - #[test] - fn emit_events_with_no_existential_deposit_suicide() { - <$ext_builder>::default() - .existential_deposit(1) - .build() - .execute_with(|| { - assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 1, 100, 0)); - - assert_eq!( - events(), - [ - RuntimeEvent::System(system::Event::NewAccount { account: 1 }), - RuntimeEvent::Balances(crate::Event::Endowed { account: 1, free_balance: 100 }), - RuntimeEvent::Balances(crate::Event::BalanceSet { who: 1, free: 100, reserved: 0 }), - ] - ); - - let res = Balances::slash(&1, 100); - assert_eq!(res, (NegativeImbalance::new(100), 0)); - - assert_eq!( - events(), - [ - RuntimeEvent::System(system::Event::KilledAccount { account: 1 }), - RuntimeEvent::Balances(crate::Event::Slashed { who: 1, amount: 100 }), - ] - ); - }); - } - - #[test] - fn slash_loop_works() { - <$ext_builder>::default() - .existential_deposit(100) - .build() - .execute_with(|| { - /* User has no reference counter, so they can die in these scenarios */ - - // SCENARIO: Slash would not kill account. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 1_000, 0)); - // Slashed completed in full - assert_eq!(Balances::slash(&1, 900), (NegativeImbalance::new(900), 0)); - // Account is still alive - assert!(System::account_exists(&1)); - System::assert_last_event(RuntimeEvent::Balances(crate::Event::Slashed { who: 1, amount: 900 })); - - // SCENARIO: Slash will kill account because not enough balance left. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 1_000, 0)); - // Slashed completed in full - assert_eq!(Balances::slash(&1, 950), (NegativeImbalance::new(950), 0)); - // Account is killed - assert!(!System::account_exists(&1)); - - // SCENARIO: Over-slash will kill account, and report missing slash amount. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 1_000, 0)); - // Slashed full free_balance, and reports 300 not slashed - assert_eq!(Balances::slash(&1, 1_300), (NegativeImbalance::new(1000), 300)); - // Account is dead - assert!(!System::account_exists(&1)); - - // SCENARIO: Over-slash can take from reserved, but keep alive. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 1_000, 400)); - // Slashed full free_balance and 300 of reserved balance - assert_eq!(Balances::slash(&1, 1_300), (NegativeImbalance::new(1300), 0)); - // Account is still alive - assert!(System::account_exists(&1)); - - // SCENARIO: Over-slash can take from reserved, and kill. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 1_000, 350)); - // Slashed full free_balance and 300 of reserved balance - assert_eq!(Balances::slash(&1, 1_300), (NegativeImbalance::new(1300), 0)); - // Account is dead because 50 reserved balance is not enough to keep alive - assert!(!System::account_exists(&1)); - - // SCENARIO: Over-slash can take as much as possible from reserved, kill, and report missing amount. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 1_000, 250)); - // Slashed full free_balance and 300 of reserved balance - assert_eq!(Balances::slash(&1, 1_300), (NegativeImbalance::new(1250), 50)); - // Account is super dead - assert!(!System::account_exists(&1)); - - /* User will now have a reference counter on them, keeping them alive in these scenarios */ - - // SCENARIO: Slash would not kill account. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 1_000, 0)); - assert_ok!(System::inc_consumers(&1)); // <-- Reference counter added here is enough for all tests - // Slashed completed in full - assert_eq!(Balances::slash(&1, 900), (NegativeImbalance::new(900), 0)); - // Account is still alive - assert!(System::account_exists(&1)); - - // SCENARIO: Slash will take as much as possible without killing account. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 1_000, 0)); - // Slashed completed in full - assert_eq!(Balances::slash(&1, 950), (NegativeImbalance::new(900), 50)); - // Account is still alive - assert!(System::account_exists(&1)); - - // SCENARIO: Over-slash will not kill account, and report missing slash amount. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 1_000, 0)); - // Slashed full free_balance minus ED, and reports 400 not slashed - assert_eq!(Balances::slash(&1, 1_300), (NegativeImbalance::new(900), 400)); - // Account is still alive - assert!(System::account_exists(&1)); - - // SCENARIO: Over-slash can take from reserved, but keep alive. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 1_000, 400)); - // Slashed full free_balance and 300 of reserved balance - assert_eq!(Balances::slash(&1, 1_300), (NegativeImbalance::new(1300), 0)); - // Account is still alive - assert!(System::account_exists(&1)); - - // SCENARIO: Over-slash can take from reserved, but keep alive. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 1_000, 350)); - // Slashed full free_balance and 250 of reserved balance to leave ED - assert_eq!(Balances::slash(&1, 1_300), (NegativeImbalance::new(1250), 50)); - // Account is still alive - assert!(System::account_exists(&1)); - - // SCENARIO: Over-slash can take as much as possible from reserved and report missing amount. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 1_000, 250)); - // Slashed full free_balance and 300 of reserved balance - assert_eq!(Balances::slash(&1, 1_300), (NegativeImbalance::new(1150), 150)); - // Account is still alive - assert!(System::account_exists(&1)); - - // Slash on non-existent account is okay. - assert_eq!(Balances::slash(&12345, 1_300), (NegativeImbalance::new(0), 1300)); - }); - } - - #[test] - fn slash_reserved_loop_works() { - <$ext_builder>::default() - .existential_deposit(100) - .build() - .execute_with(|| { - /* User has no reference counter, so they can die in these scenarios */ - - // SCENARIO: Slash would not kill account. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 50, 1_000)); - // Slashed completed in full - assert_eq!(Balances::slash_reserved(&1, 900), (NegativeImbalance::new(900), 0)); - // Account is still alive - assert!(System::account_exists(&1)); - - // SCENARIO: Slash would kill account. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 50, 1_000)); - // Slashed completed in full - assert_eq!(Balances::slash_reserved(&1, 1_000), (NegativeImbalance::new(1_000), 0)); - // Account is dead - assert!(!System::account_exists(&1)); - - // SCENARIO: Over-slash would kill account, and reports left over slash. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 50, 1_000)); - // Slashed completed in full - assert_eq!(Balances::slash_reserved(&1, 1_300), (NegativeImbalance::new(1_000), 300)); - // Account is dead - assert!(!System::account_exists(&1)); - - // SCENARIO: Over-slash does not take from free balance. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 300, 1_000)); - // Slashed completed in full - assert_eq!(Balances::slash_reserved(&1, 1_300), (NegativeImbalance::new(1_000), 300)); - // Account is alive because of free balance - assert!(System::account_exists(&1)); - - /* User has a reference counter, so they cannot die */ - - // SCENARIO: Slash would not kill account. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 50, 1_000)); - assert_ok!(System::inc_consumers(&1)); // <-- Reference counter added here is enough for all tests - // Slashed completed in full - assert_eq!(Balances::slash_reserved(&1, 900), (NegativeImbalance::new(900), 0)); - // Account is still alive - assert!(System::account_exists(&1)); - - // SCENARIO: Slash as much as possible without killing. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 50, 1_000)); - // Slashed as much as possible - assert_eq!(Balances::slash_reserved(&1, 1_000), (NegativeImbalance::new(950), 50)); - // Account is still alive - assert!(System::account_exists(&1)); - - // SCENARIO: Over-slash reports correctly, where reserved is needed to keep alive. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 50, 1_000)); - // Slashed as much as possible - assert_eq!(Balances::slash_reserved(&1, 1_300), (NegativeImbalance::new(950), 350)); - // Account is still alive - assert!(System::account_exists(&1)); - - // SCENARIO: Over-slash reports correctly, where full reserved is removed. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 200, 1_000)); - // Slashed as much as possible - assert_eq!(Balances::slash_reserved(&1, 1_300), (NegativeImbalance::new(1_000), 300)); - // Account is still alive - assert!(System::account_exists(&1)); - - // Slash on non-existent account is okay. - assert_eq!(Balances::slash_reserved(&12345, 1_300), (NegativeImbalance::new(0), 1300)); - }); - } - - #[test] - fn operations_on_dead_account_should_not_change_state() { - // These functions all use `mutate_account` which may introduce a storage change when - // the account never existed to begin with, and shouldn't exist in the end. - <$ext_builder>::default() - .existential_deposit(0) - .build() - .execute_with(|| { - assert!(!frame_system::Account::::contains_key(&1337)); - - // Unreserve - assert_storage_noop!(assert_eq!(Balances::unreserve(&1337, 42), 42)); - // Reserve - assert_noop!(Balances::reserve(&1337, 42), Error::::InsufficientBalance); - // Slash Reserve - assert_storage_noop!(assert_eq!(Balances::slash_reserved(&1337, 42).1, 42)); - // Repatriate Reserve - assert_noop!(Balances::repatriate_reserved(&1337, &1338, 42, Status::Free), Error::::DeadAccount); - // Slash - assert_storage_noop!(assert_eq!(Balances::slash(&1337, 42).1, 42)); - }); - } - - #[test] - fn transfer_keep_alive_all_free_succeed() { - <$ext_builder>::default() - .existential_deposit(100) - .build() - .execute_with(|| { - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 100, 100)); - assert_ok!(Balances::transfer_keep_alive(Some(1).into(), 2, 100)); - assert_eq!(Balances::total_balance(&1), 100); - assert_eq!(Balances::total_balance(&2), 100); - }); - } - - #[test] - fn transfer_all_works() { - <$ext_builder>::default() - .existential_deposit(100) - .build() - .execute_with(|| { - // setup - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 200, 0)); - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 2, 0, 0)); - // transfer all and allow death - assert_ok!(Balances::transfer_all(Some(1).into(), 2, false)); - assert_eq!(Balances::total_balance(&1), 0); - assert_eq!(Balances::total_balance(&2), 200); - - // setup - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 200, 0)); - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 2, 0, 0)); - // transfer all and keep alive - assert_ok!(Balances::transfer_all(Some(1).into(), 2, true)); - assert_eq!(Balances::total_balance(&1), 100); - assert_eq!(Balances::total_balance(&2), 100); - - // setup - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 200, 10)); - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 2, 0, 0)); - // transfer all and allow death w/ reserved - assert_ok!(Balances::transfer_all(Some(1).into(), 2, false)); - assert_eq!(Balances::total_balance(&1), 0); - assert_eq!(Balances::total_balance(&2), 200); - - // setup - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1, 200, 10)); - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 2, 0, 0)); - // transfer all and keep alive w/ reserved - assert_ok!(Balances::transfer_all(Some(1).into(), 2, true)); - assert_eq!(Balances::total_balance(&1), 100); - assert_eq!(Balances::total_balance(&2), 110); - }); - } - - #[test] - fn named_reserve_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 111); - - let id_1 = [1u8; 8]; - let id_2 = [2u8; 8]; - let id_3 = [3u8; 8]; - - // reserve - - assert_noop!(Balances::reserve_named(&id_1, &1, 112), Error::::InsufficientBalance); - - assert_ok!(Balances::reserve_named(&id_1, &1, 12)); - - assert_eq!(Balances::reserved_balance(1), 12); - assert_eq!(Balances::reserved_balance_named(&id_1, &1), 12); - assert_eq!(Balances::reserved_balance_named(&id_2, &1), 0); - - assert_ok!(Balances::reserve_named(&id_1, &1, 2)); - - assert_eq!(Balances::reserved_balance(1), 14); - assert_eq!(Balances::reserved_balance_named(&id_1, &1), 14); - assert_eq!(Balances::reserved_balance_named(&id_2, &1), 0); - - assert_ok!(Balances::reserve_named(&id_2, &1, 23)); - - assert_eq!(Balances::reserved_balance(1), 37); - assert_eq!(Balances::reserved_balance_named(&id_1, &1), 14); - assert_eq!(Balances::reserved_balance_named(&id_2, &1), 23); - - assert_ok!(Balances::reserve(&1, 34)); - - assert_eq!(Balances::reserved_balance(1), 71); - assert_eq!(Balances::reserved_balance_named(&id_1, &1), 14); - assert_eq!(Balances::reserved_balance_named(&id_2, &1), 23); - - assert_eq!(Balances::total_balance(&1), 111); - assert_eq!(Balances::free_balance(1), 40); - - assert_noop!(Balances::reserve_named(&id_3, &1, 2), Error::::TooManyReserves); - - // unreserve - - assert_eq!(Balances::unreserve_named(&id_1, &1, 10), 0); - - assert_eq!(Balances::reserved_balance(1), 61); - assert_eq!(Balances::reserved_balance_named(&id_1, &1), 4); - assert_eq!(Balances::reserved_balance_named(&id_2, &1), 23); - - assert_eq!(Balances::unreserve_named(&id_1, &1, 5), 1); - - assert_eq!(Balances::reserved_balance(1), 57); - assert_eq!(Balances::reserved_balance_named(&id_1, &1), 0); - assert_eq!(Balances::reserved_balance_named(&id_2, &1), 23); - - assert_eq!(Balances::unreserve_named(&id_2, &1, 3), 0); - - assert_eq!(Balances::reserved_balance(1), 54); - assert_eq!(Balances::reserved_balance_named(&id_1, &1), 0); - assert_eq!(Balances::reserved_balance_named(&id_2, &1), 20); - - assert_eq!(Balances::total_balance(&1), 111); - assert_eq!(Balances::free_balance(1), 57); - - // slash_reserved_named - - assert_ok!(Balances::reserve_named(&id_1, &1, 10)); - - assert_eq!(Balances::slash_reserved_named(&id_1, &1, 25).1, 15); - - assert_eq!(Balances::reserved_balance(1), 54); - assert_eq!(Balances::reserved_balance_named(&id_1, &1), 0); - assert_eq!(Balances::reserved_balance_named(&id_2, &1), 20); - assert_eq!(Balances::total_balance(&1), 101); - - assert_eq!(Balances::slash_reserved_named(&id_2, &1, 5).1, 0); - - assert_eq!(Balances::reserved_balance(1), 49); - assert_eq!(Balances::reserved_balance_named(&id_1, &1), 0); - assert_eq!(Balances::reserved_balance_named(&id_2, &1), 15); - assert_eq!(Balances::total_balance(&1), 96); - - // repatriate_reserved_named - - let _ = Balances::deposit_creating(&2, 100); - - assert_eq!(Balances::repatriate_reserved_named(&id_2, &1, &2, 10, Status::Reserved).unwrap(), 0); - - assert_eq!(Balances::reserved_balance_named(&id_2, &1), 5); - assert_eq!(Balances::reserved_balance_named(&id_2, &2), 10); - assert_eq!(Balances::reserved_balance(&2), 10); - - assert_eq!(Balances::repatriate_reserved_named(&id_2, &2, &1, 11, Status::Reserved).unwrap(), 1); - - assert_eq!(Balances::reserved_balance_named(&id_2, &1), 15); - assert_eq!(Balances::reserved_balance_named(&id_2, &2), 0); - assert_eq!(Balances::reserved_balance(&2), 0); - - assert_eq!(Balances::repatriate_reserved_named(&id_2, &1, &2, 10, Status::Free).unwrap(), 0); - assert_eq!(Balances::reserved_balance_named(&id_2, &1), 5); - assert_eq!(Balances::reserved_balance_named(&id_2, &2), 0); - assert_eq!(Balances::free_balance(&2), 110); - - // repatriate_reserved_named to self - - assert_eq!(Balances::repatriate_reserved_named(&id_2, &1, &1, 10, Status::Reserved).unwrap(), 5); - assert_eq!(Balances::reserved_balance_named(&id_2, &1), 5); - - assert_eq!(Balances::free_balance(&1), 47); - - assert_eq!(Balances::repatriate_reserved_named(&id_2, &1, &1, 15, Status::Free).unwrap(), 10); - assert_eq!(Balances::reserved_balance_named(&id_2, &1), 0); - - assert_eq!(Balances::free_balance(&1), 52); - }); - } - - #[test] - fn reserved_named_to_yourself_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 110); - - let id = [1u8; 8]; - - assert_ok!(Balances::reserve_named(&id, &1, 50)); - assert_ok!(Balances::repatriate_reserved_named(&id, &1, &1, 50, Status::Free), 0); - assert_eq!(Balances::free_balance(1), 110); - assert_eq!(Balances::reserved_balance_named(&id, &1), 0); - - assert_ok!(Balances::reserve_named(&id, &1, 50)); - assert_ok!(Balances::repatriate_reserved_named(&id, &1, &1, 60, Status::Free), 10); - assert_eq!(Balances::free_balance(1), 110); - assert_eq!(Balances::reserved_balance_named(&id, &1), 0); - }); - } - - #[test] - fn ensure_reserved_named_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 111); - - let id = [1u8; 8]; - - assert_ok!(Balances::ensure_reserved_named(&id, &1, 15)); - assert_eq!(Balances::reserved_balance_named(&id, &1), 15); - - assert_ok!(Balances::ensure_reserved_named(&id, &1, 10)); - assert_eq!(Balances::reserved_balance_named(&id, &1), 10); - - assert_ok!(Balances::ensure_reserved_named(&id, &1, 20)); - assert_eq!(Balances::reserved_balance_named(&id, &1), 20); - }); - } - - #[test] - fn unreserve_all_named_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 111); - - let id = [1u8; 8]; - - assert_ok!(Balances::reserve_named(&id, &1, 15)); - - assert_eq!(Balances::unreserve_all_named(&id, &1), 15); - assert_eq!(Balances::reserved_balance_named(&id, &1), 0); - assert_eq!(Balances::free_balance(&1), 111); - - assert_eq!(Balances::unreserve_all_named(&id, &1), 0); - }); - } - - #[test] - fn slash_all_reserved_named_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 111); - - let id = [1u8; 8]; - - assert_ok!(Balances::reserve_named(&id, &1, 15)); - - assert_eq!(Balances::slash_all_reserved_named(&id, &1).peek(), 15); - assert_eq!(Balances::reserved_balance_named(&id, &1), 0); - assert_eq!(Balances::free_balance(&1), 96); - - assert_eq!(Balances::slash_all_reserved_named(&id, &1).peek(), 0); - }); - } - - #[test] - fn repatriate_all_reserved_named_should_work() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 111); - let _ = Balances::deposit_creating(&2, 10); - let _ = Balances::deposit_creating(&3, 10); - - let id = [1u8; 8]; - - assert_ok!(Balances::reserve_named(&id, &1, 15)); - - assert_ok!(Balances::repatriate_all_reserved_named(&id, &1, &2, Status::Reserved)); - assert_eq!(Balances::reserved_balance_named(&id, &1), 0); - assert_eq!(Balances::reserved_balance_named(&id, &2), 15); - - assert_ok!(Balances::repatriate_all_reserved_named(&id, &2, &3, Status::Free)); - assert_eq!(Balances::reserved_balance_named(&id, &2), 0); - assert_eq!(Balances::free_balance(&3), 25); - }); - } - - #[test] - fn set_balance_handles_killing_account() { - <$ext_builder>::default().build().execute_with(|| { - let _ = Balances::deposit_creating(&1, 111); - assert_ok!(frame_system::Pallet::::inc_consumers(&1)); - assert_noop!( - Balances::set_balance(RuntimeOrigin::root(), 1, 0, 0), - DispatchError::ConsumerRemaining, - ); - }); - } - - #[test] - fn set_balance_handles_total_issuance() { - <$ext_builder>::default().build().execute_with(|| { - let old_total_issuance = Balances::total_issuance(); - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 1337, 69, 42)); - assert_eq!(Balances::total_issuance(), old_total_issuance + 69 + 42); - assert_eq!(Balances::total_balance(&1337), 69 + 42); - assert_eq!(Balances::free_balance(&1337), 69); - assert_eq!(Balances::reserved_balance(&1337), 42); - }); - } - - #[test] - fn fungible_unbalanced_trait_set_balance_works() { - <$ext_builder>::default().build().execute_with(|| { - assert_eq!(>::balance(&1337), 0); - assert_ok!(>::set_balance(&1337, 100)); - assert_eq!(>::balance(&1337), 100); - - assert_ok!(Balances::reserve(&1337, 60)); - assert_eq!(Balances::free_balance(1337) , 40); - assert_eq!(Balances::reserved_balance(1337), 60); - - assert_noop!(>::set_balance(&1337, 0), ArithmeticError::Underflow); - - assert_ok!(>::set_balance(&1337, 60)); - assert_eq!(Balances::free_balance(1337) , 0); - assert_eq!(Balances::reserved_balance(1337), 60); - }); - } - - #[test] - fn fungible_unbalanced_trait_set_total_issuance_works() { - <$ext_builder>::default().build().execute_with(|| { - assert_eq!(>::total_issuance(), 0); - >::set_total_issuance(100); - assert_eq!(>::total_issuance(), 100); - }); - } - - #[test] - fn fungible_unbalanced_trait_decrease_balance_simple_works() { - <$ext_builder>::default().build().execute_with(|| { - // An Account that starts at 100 - assert_ok!(>::set_balance(&1337, 100)); - // and reserves 50 - assert_ok!(Balances::reserve(&1337, 50)); - // and is decreased by 20 - assert_ok!(>::decrease_balance(&1337, 20)); - // should end up at 80. - assert_eq!(>::balance(&1337), 80); - }); - } - - #[test] - fn fungible_unbalanced_trait_decrease_balance_works() { - <$ext_builder>::default().build().execute_with(|| { - assert_ok!(>::set_balance(&1337, 100)); - assert_eq!(>::balance(&1337), 100); - - assert_noop!( - >::decrease_balance(&1337, 101), - TokenError::NoFunds - ); - assert_eq!( - >::decrease_balance(&1337, 100), - Ok(100) - ); - assert_eq!(>::balance(&1337), 0); - - // free: 40, reserved: 60 - assert_ok!(>::set_balance(&1337, 100)); - assert_ok!(Balances::reserve(&1337, 60)); - assert_eq!(Balances::free_balance(1337) , 40); - assert_eq!(Balances::reserved_balance(1337), 60); - assert_noop!( - >::decrease_balance(&1337, 41), - TokenError::NoFunds - ); - assert_eq!( - >::decrease_balance(&1337, 40), - Ok(40) - ); - assert_eq!(>::balance(&1337), 60); - assert_eq!(Balances::free_balance(1337), 0); - assert_eq!(Balances::reserved_balance(1337), 60); - }); - } - - #[test] - fn fungible_unbalanced_trait_decrease_balance_at_most_works() { - <$ext_builder>::default().build().execute_with(|| { - assert_ok!(>::set_balance(&1337, 100)); - assert_eq!(>::balance(&1337), 100); - - assert_eq!( - >::decrease_balance_at_most(&1337, 101), - 100 - ); - assert_eq!(>::balance(&1337), 0); - - assert_ok!(>::set_balance(&1337, 100)); - assert_eq!( - >::decrease_balance_at_most(&1337, 100), - 100 - ); - assert_eq!(>::balance(&1337), 0); - - // free: 40, reserved: 60 - assert_ok!(>::set_balance(&1337, 100)); - assert_ok!(Balances::reserve(&1337, 60)); - assert_eq!(Balances::free_balance(1337) , 40); - assert_eq!(Balances::reserved_balance(1337), 60); - assert_eq!( - >::decrease_balance_at_most(&1337, 0), - 0 - ); - assert_eq!(Balances::free_balance(1337) , 40); - assert_eq!(Balances::reserved_balance(1337), 60); - assert_eq!( - >::decrease_balance_at_most(&1337, 10), - 10 - ); - assert_eq!(Balances::free_balance(1337), 30); - assert_eq!( - >::decrease_balance_at_most(&1337, 200), - 30 - ); - assert_eq!(>::balance(&1337), 60); - assert_eq!(Balances::free_balance(1337), 0); - assert_eq!(Balances::reserved_balance(1337), 60); - }); - } - - #[test] - fn fungible_unbalanced_trait_increase_balance_works() { - <$ext_builder>::default().build().execute_with(|| { - assert_noop!( - >::increase_balance(&1337, 0), - TokenError::BelowMinimum - ); - assert_eq!( - >::increase_balance(&1337, 1), - Ok(1) - ); - assert_noop!( - >::increase_balance(&1337, u64::MAX), - ArithmeticError::Overflow - ); - }); - } - - #[test] - fn fungible_unbalanced_trait_increase_balance_at_most_works() { - <$ext_builder>::default().build().execute_with(|| { - assert_eq!( - >::increase_balance_at_most(&1337, 0), - 0 - ); - assert_eq!( - >::increase_balance_at_most(&1337, 1), - 1 - ); - assert_eq!( - >::increase_balance_at_most(&1337, u64::MAX), - u64::MAX - 1 - ); - }); - } - } -} diff --git a/frame/balances/src/tests/currency_tests.rs b/frame/balances/src/tests/currency_tests.rs new file mode 100644 index 0000000000..b0a38f4ac4 --- /dev/null +++ b/frame/balances/src/tests/currency_tests.rs @@ -0,0 +1,1231 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests regarding the functionality of the `Currency` trait set implementations. + +use super::*; +use crate::NegativeImbalance; +use frame_support::traits::{ + BalanceStatus::{Free, Reserved}, + Currency, + ExistenceRequirement::{self, AllowDeath}, + LockIdentifier, LockableCurrency, NamedReservableCurrency, ReservableCurrency, WithdrawReasons, +}; + +const ID_1: LockIdentifier = *b"1 "; +const ID_2: LockIdentifier = *b"2 "; + +pub const CALL: &::RuntimeCall = + &RuntimeCall::Balances(crate::Call::transfer_allow_death { dest: 0, value: 0 }); + +#[test] +fn basic_locking_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_eq!(Balances::free_balance(1), 10); + Balances::set_lock(ID_1, &1, 9, WithdrawReasons::all()); + assert_noop!( + >::transfer(&1, &2, 5, AllowDeath), + TokenError::Frozen + ); + }); +} + +#[test] +fn account_should_be_reaped() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_eq!(Balances::free_balance(1), 10); + assert_ok!(>::transfer(&1, &2, 10, AllowDeath)); + assert_eq!(System::providers(&1), 0); + assert_eq!(System::consumers(&1), 0); + // Check that the account is dead. + assert!(!frame_system::Account::::contains_key(&1)); + }); +} + +#[test] +fn reap_failed_due_to_provider_and_consumer() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + // SCENARIO: only one provider and there are remaining consumers. + assert_ok!(System::inc_consumers(&1)); + assert!(!System::can_dec_provider(&1)); + assert_noop!( + >::transfer(&1, &2, 10, AllowDeath), + TokenError::Frozen + ); + assert!(System::account_exists(&1)); + assert_eq!(Balances::free_balance(1), 10); + + // SCENARIO: more than one provider, but will not kill account due to other provider. + assert_eq!(System::inc_providers(&1), frame_system::IncRefStatus::Existed); + assert_eq!(System::providers(&1), 2); + assert!(System::can_dec_provider(&1)); + assert_ok!(>::transfer(&1, &2, 10, AllowDeath)); + assert_eq!(System::providers(&1), 1); + assert!(System::account_exists(&1)); + assert_eq!(Balances::free_balance(1), 0); + }); +} + +#[test] +fn partial_locking_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &1, 5, WithdrawReasons::all()); + assert_ok!(>::transfer(&1, &2, 1, AllowDeath)); + }); +} + +#[test] +fn lock_removal_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &1, u64::MAX, WithdrawReasons::all()); + Balances::remove_lock(ID_1, &1); + assert_ok!(>::transfer(&1, &2, 1, AllowDeath)); + }); +} + +#[test] +fn lock_replacement_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &1, u64::MAX, WithdrawReasons::all()); + Balances::set_lock(ID_1, &1, 5, WithdrawReasons::all()); + assert_ok!(>::transfer(&1, &2, 1, AllowDeath)); + }); +} + +#[test] +fn double_locking_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &1, 5, WithdrawReasons::all()); + Balances::set_lock(ID_2, &1, 5, WithdrawReasons::all()); + assert_ok!(>::transfer(&1, &2, 1, AllowDeath)); + }); +} + +#[test] +fn combination_locking_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &1, u64::MAX, WithdrawReasons::empty()); + Balances::set_lock(ID_2, &1, 0, WithdrawReasons::all()); + assert_ok!(>::transfer(&1, &2, 1, AllowDeath)); + }); +} + +#[test] +fn lock_value_extension_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &1, 5, WithdrawReasons::all()); + assert_noop!( + >::transfer(&1, &2, 6, AllowDeath), + TokenError::Frozen + ); + Balances::extend_lock(ID_1, &1, 2, WithdrawReasons::all()); + assert_noop!( + >::transfer(&1, &2, 6, AllowDeath), + TokenError::Frozen + ); + Balances::extend_lock(ID_1, &1, 8, WithdrawReasons::all()); + assert_noop!( + >::transfer(&1, &2, 3, AllowDeath), + TokenError::Frozen + ); + }); +} + +#[test] +fn lock_should_work_reserve() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + pallet_transaction_payment::NextFeeMultiplier::::put( + Multiplier::saturating_from_integer(1), + ); + Balances::set_lock(ID_1, &1, 10, WithdrawReasons::RESERVE); + assert_noop!( + >::transfer(&1, &2, 1, AllowDeath), + TokenError::Frozen + ); + assert_noop!(Balances::reserve(&1, 1), Error::::LiquidityRestrictions,); + assert!( as SignedExtension>::pre_dispatch( + ChargeTransactionPayment::from(1), + &1, + CALL, + &info_from_weight(Weight::from_parts(1, 0)), + 1, + ) + .is_err()); + assert!( as SignedExtension>::pre_dispatch( + ChargeTransactionPayment::from(0), + &1, + CALL, + &info_from_weight(Weight::from_parts(1, 0)), + 1, + ) + .is_err()); + }); +} + +#[test] +fn lock_should_work_tx_fee() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &1, 10, WithdrawReasons::TRANSACTION_PAYMENT); + assert_noop!( + >::transfer(&1, &2, 1, AllowDeath), + TokenError::Frozen + ); + assert_noop!(Balances::reserve(&1, 1), Error::::LiquidityRestrictions,); + assert!( as SignedExtension>::pre_dispatch( + ChargeTransactionPayment::from(1), + &1, + CALL, + &info_from_weight(Weight::from_parts(1, 0)), + 1, + ) + .is_err()); + assert!( as SignedExtension>::pre_dispatch( + ChargeTransactionPayment::from(0), + &1, + CALL, + &info_from_weight(Weight::from_parts(1, 0)), + 1, + ) + .is_err()); + }); +} + +#[test] +fn lock_block_number_extension_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &1, 10, WithdrawReasons::all()); + assert_noop!( + >::transfer(&1, &2, 6, AllowDeath), + TokenError::Frozen + ); + Balances::extend_lock(ID_1, &1, 10, WithdrawReasons::all()); + assert_noop!( + >::transfer(&1, &2, 6, AllowDeath), + TokenError::Frozen + ); + System::set_block_number(2); + Balances::extend_lock(ID_1, &1, 10, WithdrawReasons::all()); + assert_noop!( + >::transfer(&1, &2, 3, AllowDeath), + TokenError::Frozen + ); + }); +} + +#[test] +fn lock_reasons_extension_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &1, 10, WithdrawReasons::TRANSFER); + assert_noop!( + >::transfer(&1, &2, 6, AllowDeath), + TokenError::Frozen + ); + Balances::extend_lock(ID_1, &1, 10, WithdrawReasons::empty()); + assert_noop!( + >::transfer(&1, &2, 6, AllowDeath), + TokenError::Frozen + ); + Balances::extend_lock(ID_1, &1, 10, WithdrawReasons::RESERVE); + assert_noop!( + >::transfer(&1, &2, 6, AllowDeath), + TokenError::Frozen + ); + }); +} + +#[test] +fn reserved_balance_should_prevent_reclaim_count() { + ExtBuilder::default() + .existential_deposit(256 * 1) + .monied(true) + .build_and_execute_with(|| { + System::inc_account_nonce(&2); + assert_eq!(Balances::total_balance(&2), 256 * 20); + assert_eq!(System::providers(&2), 1); + System::inc_providers(&2); + assert_eq!(System::providers(&2), 2); + + assert_ok!(Balances::reserve(&2, 256 * 19 + 1)); // account 2 becomes mostly reserved + assert_eq!(System::providers(&2), 1); + assert_eq!(Balances::free_balance(2), 255); // "free" account would be deleted. + assert_eq!(Balances::total_balance(&2), 256 * 20); // reserve still exists. + assert_eq!(System::account_nonce(&2), 1); + + // account 4 tries to take index 1 for account 5. + assert_ok!(Balances::transfer_allow_death(Some(4).into(), 5, 256 * 1 + 0x69)); + assert_eq!(Balances::total_balance(&5), 256 * 1 + 0x69); + + assert!(Balances::slash_reserved(&2, 256 * 19 + 1).1.is_zero()); // account 2 gets slashed + + // "reserve" account reduced to 255 (below ED) so account no longer consuming + assert_ok!(System::dec_providers(&2)); + assert_eq!(System::providers(&2), 0); + // account deleted + assert_eq!(System::account_nonce(&2), 0); // nonce zero + assert_eq!(Balances::total_balance(&2), 0); + + // account 4 tries to take index 1 again for account 6. + assert_ok!(Balances::transfer_allow_death(Some(4).into(), 6, 256 * 1 + 0x69)); + assert_eq!(Balances::total_balance(&6), 256 * 1 + 0x69); + }); +} + +#[test] +fn reward_should_work() { + ExtBuilder::default().monied(true).build_and_execute_with(|| { + assert_eq!(Balances::total_balance(&1), 10); + assert_ok!(Balances::deposit_into_existing(&1, 10).map(drop)); + System::assert_last_event(RuntimeEvent::Balances(crate::Event::Deposit { + who: 1, + amount: 10, + })); + assert_eq!(Balances::total_balance(&1), 20); + assert_eq!(Balances::total_issuance(), 120); + }); +} + +#[test] +fn balance_works() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 42); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit { + who: 1, + amount: 42, + })); + assert_eq!(Balances::free_balance(1), 42); + assert_eq!(Balances::reserved_balance(1), 0); + assert_eq!(Balances::total_balance(&1), 42); + assert_eq!(Balances::free_balance(2), 0); + assert_eq!(Balances::reserved_balance(2), 0); + assert_eq!(Balances::total_balance(&2), 0); + }); +} + +#[test] +fn reserving_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 111); + + assert_eq!(Balances::total_balance(&1), 111); + assert_eq!(Balances::free_balance(1), 111); + assert_eq!(Balances::reserved_balance(1), 0); + + assert_ok!(Balances::reserve(&1, 69)); + + assert_eq!(Balances::total_balance(&1), 111); + assert_eq!(Balances::free_balance(1), 42); + assert_eq!(Balances::reserved_balance(1), 69); + }); +} + +#[test] +fn deducting_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 111); + assert_ok!(Balances::reserve(&1, 69)); + assert_eq!(Balances::free_balance(1), 42); + }); +} + +#[test] +fn refunding_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 42); + assert_ok!(Balances::mutate_account(&1, |a| a.reserved = 69)); + Balances::unreserve(&1, 69); + assert_eq!(Balances::free_balance(1), 111); + assert_eq!(Balances::reserved_balance(1), 0); + }); +} + +#[test] +fn slashing_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 112); + assert_ok!(Balances::reserve(&1, 69)); + assert!(Balances::slash(&1, 42).1.is_zero()); + assert_eq!(Balances::free_balance(1), 1); + assert_eq!(Balances::reserved_balance(1), 69); + assert_eq!(Balances::total_issuance(), 70); + }); +} + +#[test] +fn withdrawing_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&2, 111); + let _ = + Balances::withdraw(&2, 11, WithdrawReasons::TRANSFER, ExistenceRequirement::KeepAlive); + System::assert_last_event(RuntimeEvent::Balances(crate::Event::Withdraw { + who: 2, + amount: 11, + })); + assert_eq!(Balances::free_balance(2), 100); + assert_eq!(Balances::total_issuance(), 100); + }); +} + +#[test] +fn withdrawing_balance_should_fail_when_not_expendable() { + ExtBuilder::default().build_and_execute_with(|| { + ExistentialDeposit::set(10); + let _ = Balances::deposit_creating(&2, 20); + assert_ok!(Balances::reserve(&2, 5)); + assert_noop!( + Balances::withdraw(&2, 6, WithdrawReasons::TRANSFER, ExistenceRequirement::KeepAlive), + Error::::Expendability, + ); + assert_ok!(Balances::withdraw( + &2, + 5, + WithdrawReasons::TRANSFER, + ExistenceRequirement::KeepAlive + ),); + }); +} + +#[test] +fn slashing_incomplete_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 42); + assert_ok!(Balances::reserve(&1, 21)); + assert_eq!(Balances::slash(&1, 69).1, 49); + assert_eq!(Balances::free_balance(1), 1); + assert_eq!(Balances::reserved_balance(1), 21); + assert_eq!(Balances::total_issuance(), 22); + }); +} + +#[test] +fn unreserving_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 111); + assert_ok!(Balances::reserve(&1, 110)); + Balances::unreserve(&1, 41); + assert_eq!(Balances::reserved_balance(1), 69); + assert_eq!(Balances::free_balance(1), 42); + }); +} + +#[test] +fn slashing_reserved_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 112); + assert_ok!(Balances::reserve(&1, 111)); + assert_eq!(Balances::slash_reserved(&1, 42).1, 0); + assert_eq!(Balances::reserved_balance(1), 69); + assert_eq!(Balances::free_balance(1), 1); + assert_eq!(Balances::total_issuance(), 70); + }); +} + +#[test] +fn slashing_incomplete_reserved_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 111); + assert_ok!(Balances::reserve(&1, 42)); + assert_eq!(Balances::slash_reserved(&1, 69).1, 27); + assert_eq!(Balances::free_balance(1), 69); + assert_eq!(Balances::reserved_balance(1), 0); + assert_eq!(Balances::total_issuance(), 69); + }); +} + +#[test] +fn repatriating_reserved_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 111); + let _ = Balances::deposit_creating(&2, 1); + assert_ok!(Balances::reserve(&1, 110)); + assert_ok!(Balances::repatriate_reserved(&1, &2, 41, Free), 0); + System::assert_last_event(RuntimeEvent::Balances(crate::Event::ReserveRepatriated { + from: 1, + to: 2, + amount: 41, + destination_status: Free, + })); + assert_eq!(Balances::reserved_balance(1), 69); + assert_eq!(Balances::free_balance(1), 1); + assert_eq!(Balances::reserved_balance(2), 0); + assert_eq!(Balances::free_balance(2), 42); + }); +} + +#[test] +fn transferring_reserved_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 111); + let _ = Balances::deposit_creating(&2, 1); + assert_ok!(Balances::reserve(&1, 110)); + assert_ok!(Balances::repatriate_reserved(&1, &2, 41, Reserved), 0); + assert_eq!(Balances::reserved_balance(1), 69); + assert_eq!(Balances::free_balance(1), 1); + assert_eq!(Balances::reserved_balance(2), 41); + assert_eq!(Balances::free_balance(2), 1); + }); +} + +#[test] +fn transferring_reserved_balance_to_yourself_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 110); + assert_ok!(Balances::reserve(&1, 50)); + assert_ok!(Balances::repatriate_reserved(&1, &1, 50, Free), 0); + assert_eq!(Balances::free_balance(1), 110); + assert_eq!(Balances::reserved_balance(1), 0); + + assert_ok!(Balances::reserve(&1, 50)); + assert_ok!(Balances::repatriate_reserved(&1, &1, 60, Free), 10); + assert_eq!(Balances::free_balance(1), 110); + assert_eq!(Balances::reserved_balance(1), 0); + }); +} + +#[test] +fn transferring_reserved_balance_to_nonexistent_should_fail() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 111); + assert_ok!(Balances::reserve(&1, 110)); + assert_noop!( + Balances::repatriate_reserved(&1, &2, 42, Free), + Error::::DeadAccount + ); + }); +} + +#[test] +fn transferring_incomplete_reserved_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 110); + let _ = Balances::deposit_creating(&2, 1); + assert_ok!(Balances::reserve(&1, 41)); + assert_ok!(Balances::repatriate_reserved(&1, &2, 69, Free), 28); + assert_eq!(Balances::reserved_balance(1), 0); + assert_eq!(Balances::free_balance(1), 69); + assert_eq!(Balances::reserved_balance(2), 0); + assert_eq!(Balances::free_balance(2), 42); + }); +} + +#[test] +fn transferring_too_high_value_should_not_panic() { + ExtBuilder::default().build_and_execute_with(|| { + Balances::make_free_balance_be(&1, u64::MAX); + Balances::make_free_balance_be(&2, 1); + + assert_err!( + >::transfer(&1, &2, u64::MAX, AllowDeath), + ArithmeticError::Overflow, + ); + + assert_eq!(Balances::free_balance(1), u64::MAX); + assert_eq!(Balances::free_balance(2), 1); + }); +} + +#[test] +fn account_create_on_free_too_low_with_other() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 100); + assert_eq!(Balances::total_issuance(), 100); + + // No-op. + let _ = Balances::deposit_creating(&2, 50); + assert_eq!(Balances::free_balance(2), 0); + assert_eq!(Balances::total_issuance(), 100); + }) +} + +#[test] +fn account_create_on_free_too_low() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + // No-op. + let _ = Balances::deposit_creating(&2, 50); + assert_eq!(Balances::free_balance(2), 0); + assert_eq!(Balances::total_issuance(), 0); + }) +} + +#[test] +fn account_removal_on_free_too_low() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + assert_eq!(Balances::total_issuance(), 0); + + // Setup two accounts with free balance above the existential threshold. + let _ = Balances::deposit_creating(&1, 110); + let _ = Balances::deposit_creating(&2, 110); + + assert_eq!(Balances::free_balance(1), 110); + assert_eq!(Balances::free_balance(2), 110); + assert_eq!(Balances::total_issuance(), 220); + + // Transfer funds from account 1 of such amount that after this transfer + // the balance of account 1 will be below the existential threshold. + // This should lead to the removal of all balance of this account. + assert_ok!(Balances::transfer_allow_death(Some(1).into(), 2, 20)); + + // Verify free balance removal of account 1. + assert_eq!(Balances::free_balance(1), 0); + assert_eq!(Balances::free_balance(2), 130); + + // Verify that TotalIssuance tracks balance removal when free balance is too low. + assert_eq!(Balances::total_issuance(), 130); + }); +} + +#[test] +fn burn_must_work() { + ExtBuilder::default().monied(true).build_and_execute_with(|| { + let init_total_issuance = Balances::total_issuance(); + let imbalance = Balances::burn(10); + assert_eq!(Balances::total_issuance(), init_total_issuance - 10); + drop(imbalance); + assert_eq!(Balances::total_issuance(), init_total_issuance); + }); +} + +#[test] +#[should_panic = "the balance of any account should always be at least the existential deposit."] +fn cannot_set_genesis_value_below_ed() { + EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = 11); + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + let _ = crate::GenesisConfig:: { balances: vec![(1, 10)] } + .assimilate_storage(&mut t) + .unwrap(); +} + +#[test] +#[should_panic = "duplicate balances in genesis."] +fn cannot_set_genesis_value_twice() { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + let _ = crate::GenesisConfig:: { balances: vec![(1, 10), (2, 20), (1, 15)] } + .assimilate_storage(&mut t) + .unwrap(); +} + +#[test] +fn existential_deposit_respected_when_reserving() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + // Set balance to free and reserved at the existential deposit + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 1, 101)); + // Check balance + assert_eq!(Balances::free_balance(1), 101); + assert_eq!(Balances::reserved_balance(1), 0); + + // Reserve some free balance + assert_ok!(Balances::reserve(&1, 1)); + // Check balance, the account should be ok. + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::reserved_balance(1), 1); + + // Cannot reserve any more of the free balance. + assert_noop!(Balances::reserve(&1, 1), DispatchError::ConsumerRemaining); + }); +} + +#[test] +fn slash_fails_when_account_needed() { + ExtBuilder::default().existential_deposit(50).build_and_execute_with(|| { + // Set balance to free and reserved at the existential deposit + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 1, 52)); + assert_ok!(Balances::reserve(&1, 1)); + // Check balance + assert_eq!(Balances::free_balance(1), 51); + assert_eq!(Balances::reserved_balance(1), 1); + + // Slash a small amount + let res = Balances::slash(&1, 1); + assert_eq!(res, (NegativeImbalance::new(1), 0)); + + // The account should be dead. + assert_eq!(Balances::free_balance(1), 50); + assert_eq!(Balances::reserved_balance(1), 1); + + // Slashing again doesn't work since we require the ED + let res = Balances::slash(&1, 1); + assert_eq!(res, (NegativeImbalance::new(0), 1)); + + // The account should be dead. + assert_eq!(Balances::free_balance(1), 50); + assert_eq!(Balances::reserved_balance(1), 1); + }); +} + +#[test] +fn account_deleted_when_just_dust() { + ExtBuilder::default().existential_deposit(50).build_and_execute_with(|| { + // Set balance to free and reserved at the existential deposit + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 1, 50)); + // Check balance + assert_eq!(Balances::free_balance(1), 50); + + // Slash a small amount + let res = Balances::slash(&1, 1); + assert_eq!(res, (NegativeImbalance::new(1), 0)); + + // The account should be dead. + assert_eq!(Balances::free_balance(1), 0); + }); +} + +#[test] +fn emit_events_with_reserve_and_unreserve() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 100); + + System::set_block_number(2); + assert_ok!(Balances::reserve(&1, 10)); + + System::assert_last_event(RuntimeEvent::Balances(crate::Event::Reserved { + who: 1, + amount: 10, + })); + + System::set_block_number(3); + assert!(Balances::unreserve(&1, 5).is_zero()); + + System::assert_last_event(RuntimeEvent::Balances(crate::Event::Unreserved { + who: 1, + amount: 5, + })); + + System::set_block_number(4); + assert_eq!(Balances::unreserve(&1, 6), 1); + + // should only unreserve 5 + System::assert_last_event(RuntimeEvent::Balances(crate::Event::Unreserved { + who: 1, + amount: 5, + })); + }); +} + +#[test] +fn emit_events_with_existential_deposit() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 1, 100)); + + assert_eq!( + events(), + [ + RuntimeEvent::System(system::Event::NewAccount { account: 1 }), + RuntimeEvent::Balances(crate::Event::Endowed { account: 1, free_balance: 100 }), + RuntimeEvent::Balances(crate::Event::BalanceSet { who: 1, free: 100 }), + ] + ); + + let res = Balances::slash(&1, 1); + assert_eq!(res, (NegativeImbalance::new(1), 0)); + + assert_eq!( + events(), + [ + RuntimeEvent::System(system::Event::KilledAccount { account: 1 }), + RuntimeEvent::Balances(crate::Event::DustLost { account: 1, amount: 99 }), + RuntimeEvent::Balances(crate::Event::Slashed { who: 1, amount: 1 }), + ] + ); + }); +} + +#[test] +fn emit_events_with_no_existential_deposit_suicide() { + ExtBuilder::default().existential_deposit(1).build_and_execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_eq!( + events(), + [ + RuntimeEvent::Balances(crate::Event::BalanceSet { who: 1, free: 100 }), + RuntimeEvent::System(system::Event::NewAccount { account: 1 }), + RuntimeEvent::Balances(crate::Event::Endowed { account: 1, free_balance: 100 }), + ] + ); + + let res = Balances::slash(&1, 100); + assert_eq!(res, (NegativeImbalance::new(100), 0)); + + assert_eq!( + events(), + [ + RuntimeEvent::System(system::Event::KilledAccount { account: 1 }), + RuntimeEvent::Balances(crate::Event::Slashed { who: 1, amount: 100 }), + ] + ); + }); +} + +#[test] +fn slash_over_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + // SCENARIO: Over-slash will kill account, and report missing slash amount. + Balances::make_free_balance_be(&1, 1_000); + // Slashed full free_balance, and reports 300 not slashed + assert_eq!(Balances::slash(&1, 1_300), (NegativeImbalance::new(1000), 300)); + // Account is dead + assert!(!System::account_exists(&1)); + }); +} + +#[test] +fn slash_full_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&1, 1_000); + // Slashed completed in full + assert_eq!(Balances::slash(&1, 1_000), (NegativeImbalance::new(1000), 0)); + // Account is still alive + assert!(!System::account_exists(&1)); + System::assert_last_event(RuntimeEvent::Balances(crate::Event::Slashed { + who: 1, + amount: 1000, + })); + }); +} + +#[test] +fn slash_partial_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&1, 1_000); + // Slashed completed in full + assert_eq!(Balances::slash(&1, 900), (NegativeImbalance::new(900), 0)); + // Account is still alive + assert!(System::account_exists(&1)); + System::assert_last_event(RuntimeEvent::Balances(crate::Event::Slashed { + who: 1, + amount: 900, + })); + }); +} + +#[test] +fn slash_dusting_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&1, 1_000); + // Slashed completed in full + assert_eq!(Balances::slash(&1, 950), (NegativeImbalance::new(950), 0)); + assert!(!System::account_exists(&1)); + System::assert_last_event(RuntimeEvent::Balances(crate::Event::Slashed { + who: 1, + amount: 950, + })); + }); +} + +#[test] +fn slash_does_not_take_from_reserve() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&1, 1_000); + assert_ok!(Balances::reserve(&1, 100)); + // Slashed completed in full + assert_eq!(Balances::slash(&1, 900), (NegativeImbalance::new(800), 100)); + assert_eq!(Balances::reserved_balance(&1), 100); + System::assert_last_event(RuntimeEvent::Balances(crate::Event::Slashed { + who: 1, + amount: 800, + })); + }); +} + +#[test] +fn slash_consumed_slash_full_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&1, 1_000); + assert_ok!(System::inc_consumers(&1)); // <-- Reference counter added here is enough for all tests + // Slashed completed in full + assert_eq!(Balances::slash(&1, 900), (NegativeImbalance::new(900), 0)); + // Account is still alive + assert!(System::account_exists(&1)); + }); +} + +#[test] +fn slash_consumed_slash_over_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&1, 1_000); + assert_ok!(System::inc_consumers(&1)); // <-- Reference counter added here is enough for all tests + // Slashed completed in full + assert_eq!(Balances::slash(&1, 1_000), (NegativeImbalance::new(900), 100)); + // Account is still alive + assert!(System::account_exists(&1)); + }); +} + +#[test] +fn slash_consumed_slash_partial_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&1, 1_000); + assert_ok!(System::inc_consumers(&1)); // <-- Reference counter added here is enough for all tests + // Slashed completed in full + assert_eq!(Balances::slash(&1, 800), (NegativeImbalance::new(800), 0)); + // Account is still alive + assert!(System::account_exists(&1)); + }); +} + +#[test] +fn slash_on_non_existant_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + // Slash on non-existent account is okay. + assert_eq!(Balances::slash(&12345, 1_300), (NegativeImbalance::new(0), 1300)); + }); +} + +#[test] +fn slash_reserved_slash_partial_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&1, 1_000); + assert_ok!(Balances::reserve(&1, 900)); + // Slashed completed in full + assert_eq!(Balances::slash_reserved(&1, 800), (NegativeImbalance::new(800), 0)); + assert_eq!(System::consumers(&1), 1); + assert_eq!(Balances::reserved_balance(&1), 100); + assert_eq!(Balances::free_balance(&1), 100); + }); +} + +#[test] +fn slash_reserved_slash_everything_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&1, 1_000); + assert_ok!(Balances::reserve(&1, 900)); + assert_eq!(System::consumers(&1), 1); + // Slashed completed in full + assert_eq!(Balances::slash_reserved(&1, 900), (NegativeImbalance::new(900), 0)); + assert_eq!(System::consumers(&1), 0); + // Account is still alive + assert!(System::account_exists(&1)); + }); +} + +#[test] +fn slash_reserved_overslash_does_not_touch_free_balance() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + // SCENARIO: Over-slash doesn't touch free balance. + Balances::make_free_balance_be(&1, 1_000); + assert_ok!(Balances::reserve(&1, 800)); + // Slashed done + assert_eq!(Balances::slash_reserved(&1, 900), (NegativeImbalance::new(800), 100)); + assert_eq!(Balances::free_balance(&1), 200); + }); +} + +#[test] +fn slash_reserved_on_non_existant_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + // Slash on non-existent account is okay. + assert_eq!(Balances::slash_reserved(&12345, 1_300), (NegativeImbalance::new(0), 1300)); + }); +} + +#[test] +fn operations_on_dead_account_should_not_change_state() { + // These functions all use `mutate_account` which may introduce a storage change when + // the account never existed to begin with, and shouldn't exist in the end. + ExtBuilder::default().existential_deposit(0).build_and_execute_with(|| { + assert!(!frame_system::Account::::contains_key(&1337)); + + // Unreserve + assert_storage_noop!(assert_eq!(Balances::unreserve(&1337, 42), 42)); + // Reserve + assert_noop!(Balances::reserve(&1337, 42), Error::::InsufficientBalance); + // Slash Reserve + assert_storage_noop!(assert_eq!(Balances::slash_reserved(&1337, 42).1, 42)); + // Repatriate Reserve + assert_noop!( + Balances::repatriate_reserved(&1337, &1338, 42, Free), + Error::::DeadAccount + ); + // Slash + assert_storage_noop!(assert_eq!(Balances::slash(&1337, 42).1, 42)); + }); +} + +#[test] +fn named_reserve_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 111); + + let id_1 = TestId::Foo; + let id_2 = TestId::Bar; + let id_3 = TestId::Baz; + + // reserve + + assert_noop!( + Balances::reserve_named(&id_1, &1, 112), + Error::::InsufficientBalance + ); + + assert_ok!(Balances::reserve_named(&id_1, &1, 12)); + + assert_eq!(Balances::reserved_balance(1), 12); + assert_eq!(Balances::reserved_balance_named(&id_1, &1), 12); + assert_eq!(Balances::reserved_balance_named(&id_2, &1), 0); + + assert_ok!(Balances::reserve_named(&id_1, &1, 2)); + + assert_eq!(Balances::reserved_balance(1), 14); + assert_eq!(Balances::reserved_balance_named(&id_1, &1), 14); + assert_eq!(Balances::reserved_balance_named(&id_2, &1), 0); + + assert_ok!(Balances::reserve_named(&id_2, &1, 23)); + + assert_eq!(Balances::reserved_balance(1), 37); + assert_eq!(Balances::reserved_balance_named(&id_1, &1), 14); + assert_eq!(Balances::reserved_balance_named(&id_2, &1), 23); + + assert_ok!(Balances::reserve(&1, 34)); + + assert_eq!(Balances::reserved_balance(1), 71); + assert_eq!(Balances::reserved_balance_named(&id_1, &1), 14); + assert_eq!(Balances::reserved_balance_named(&id_2, &1), 23); + + assert_eq!(Balances::total_balance(&1), 111); + assert_eq!(Balances::free_balance(1), 40); + + assert_noop!(Balances::reserve_named(&id_3, &1, 2), Error::::TooManyReserves); + + // unreserve + + assert_eq!(Balances::unreserve_named(&id_1, &1, 10), 0); + + assert_eq!(Balances::reserved_balance(1), 61); + assert_eq!(Balances::reserved_balance_named(&id_1, &1), 4); + assert_eq!(Balances::reserved_balance_named(&id_2, &1), 23); + + assert_eq!(Balances::unreserve_named(&id_1, &1, 5), 1); + + assert_eq!(Balances::reserved_balance(1), 57); + assert_eq!(Balances::reserved_balance_named(&id_1, &1), 0); + assert_eq!(Balances::reserved_balance_named(&id_2, &1), 23); + + assert_eq!(Balances::unreserve_named(&id_2, &1, 3), 0); + + assert_eq!(Balances::reserved_balance(1), 54); + assert_eq!(Balances::reserved_balance_named(&id_1, &1), 0); + assert_eq!(Balances::reserved_balance_named(&id_2, &1), 20); + + assert_eq!(Balances::total_balance(&1), 111); + assert_eq!(Balances::free_balance(1), 57); + + // slash_reserved_named + + assert_ok!(Balances::reserve_named(&id_1, &1, 10)); + + assert_eq!(Balances::slash_reserved_named(&id_1, &1, 25).1, 15); + + assert_eq!(Balances::reserved_balance(1), 54); + assert_eq!(Balances::reserved_balance_named(&id_1, &1), 0); + assert_eq!(Balances::reserved_balance_named(&id_2, &1), 20); + assert_eq!(Balances::total_balance(&1), 101); + + assert_eq!(Balances::slash_reserved_named(&id_2, &1, 5).1, 0); + + assert_eq!(Balances::reserved_balance(1), 49); + assert_eq!(Balances::reserved_balance_named(&id_1, &1), 0); + assert_eq!(Balances::reserved_balance_named(&id_2, &1), 15); + assert_eq!(Balances::total_balance(&1), 96); + + // repatriate_reserved_named + + let _ = Balances::deposit_creating(&2, 100); + + assert_eq!(Balances::repatriate_reserved_named(&id_2, &1, &2, 10, Reserved).unwrap(), 0); + + assert_eq!(Balances::reserved_balance_named(&id_2, &1), 5); + assert_eq!(Balances::reserved_balance_named(&id_2, &2), 10); + assert_eq!(Balances::reserved_balance(&2), 10); + + assert_eq!(Balances::repatriate_reserved_named(&id_2, &2, &1, 11, Reserved).unwrap(), 1); + + assert_eq!(Balances::reserved_balance_named(&id_2, &1), 15); + assert_eq!(Balances::reserved_balance_named(&id_2, &2), 0); + assert_eq!(Balances::reserved_balance(&2), 0); + + assert_eq!(Balances::repatriate_reserved_named(&id_2, &1, &2, 10, Free).unwrap(), 0); + assert_eq!(Balances::reserved_balance_named(&id_2, &1), 5); + assert_eq!(Balances::reserved_balance_named(&id_2, &2), 0); + assert_eq!(Balances::free_balance(&2), 110); + + // repatriate_reserved_named to self + + assert_eq!(Balances::repatriate_reserved_named(&id_2, &1, &1, 10, Reserved).unwrap(), 5); + assert_eq!(Balances::reserved_balance_named(&id_2, &1), 5); + + assert_eq!(Balances::free_balance(&1), 47); + + assert_eq!(Balances::repatriate_reserved_named(&id_2, &1, &1, 15, Free).unwrap(), 10); + assert_eq!(Balances::reserved_balance_named(&id_2, &1), 0); + + assert_eq!(Balances::free_balance(&1), 52); + }); +} + +#[test] +fn reserved_named_to_yourself_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 110); + + let id = TestId::Foo; + + assert_ok!(Balances::reserve_named(&id, &1, 50)); + assert_ok!(Balances::repatriate_reserved_named(&id, &1, &1, 50, Free), 0); + assert_eq!(Balances::free_balance(1), 110); + assert_eq!(Balances::reserved_balance_named(&id, &1), 0); + + assert_ok!(Balances::reserve_named(&id, &1, 50)); + assert_ok!(Balances::repatriate_reserved_named(&id, &1, &1, 60, Free), 10); + assert_eq!(Balances::free_balance(1), 110); + assert_eq!(Balances::reserved_balance_named(&id, &1), 0); + }); +} + +#[test] +fn ensure_reserved_named_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 111); + + let id = TestId::Foo; + + assert_ok!(Balances::ensure_reserved_named(&id, &1, 15)); + assert_eq!(Balances::reserved_balance_named(&id, &1), 15); + + assert_ok!(Balances::ensure_reserved_named(&id, &1, 10)); + assert_eq!(Balances::reserved_balance_named(&id, &1), 10); + + assert_ok!(Balances::ensure_reserved_named(&id, &1, 20)); + assert_eq!(Balances::reserved_balance_named(&id, &1), 20); + }); +} + +#[test] +fn unreserve_all_named_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 111); + + let id = TestId::Foo; + + assert_ok!(Balances::reserve_named(&id, &1, 15)); + + assert_eq!(Balances::unreserve_all_named(&id, &1), 15); + assert_eq!(Balances::reserved_balance_named(&id, &1), 0); + assert_eq!(Balances::free_balance(&1), 111); + + assert_eq!(Balances::unreserve_all_named(&id, &1), 0); + }); +} + +#[test] +fn slash_all_reserved_named_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 111); + + let id = TestId::Foo; + + assert_ok!(Balances::reserve_named(&id, &1, 15)); + + assert_eq!(Balances::slash_all_reserved_named(&id, &1).peek(), 15); + assert_eq!(Balances::reserved_balance_named(&id, &1), 0); + assert_eq!(Balances::free_balance(&1), 96); + + assert_eq!(Balances::slash_all_reserved_named(&id, &1).peek(), 0); + }); +} + +#[test] +fn repatriate_all_reserved_named_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&1, 111); + let _ = Balances::deposit_creating(&2, 10); + let _ = Balances::deposit_creating(&3, 10); + + let id = TestId::Foo; + + assert_ok!(Balances::reserve_named(&id, &1, 15)); + + assert_ok!(Balances::repatriate_all_reserved_named(&id, &1, &2, Reserved)); + assert_eq!(Balances::reserved_balance_named(&id, &1), 0); + assert_eq!(Balances::reserved_balance_named(&id, &2), 15); + + assert_ok!(Balances::repatriate_all_reserved_named(&id, &2, &3, Free)); + assert_eq!(Balances::reserved_balance_named(&id, &2), 0); + assert_eq!(Balances::free_balance(&3), 25); + }); +} + +#[test] +fn freezing_and_locking_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(>::set_freeze(&TestId::Foo, &1, 4)); + Balances::set_lock(ID_1, &1, 5, WithdrawReasons::all()); + assert_eq!(System::consumers(&1), 2); + assert_eq!(Balances::account(&1).frozen, 5); + assert_ok!(>::set_freeze(&TestId::Foo, &1, 6)); + assert_eq!(Balances::account(&1).frozen, 6); + assert_ok!(>::set_freeze(&TestId::Foo, &1, 4)); + assert_eq!(Balances::account(&1).frozen, 5); + Balances::set_lock(ID_1, &1, 3, WithdrawReasons::all()); + assert_eq!(Balances::account(&1).frozen, 4); + Balances::set_lock(ID_1, &1, 5, WithdrawReasons::all()); + assert_eq!(Balances::account(&1).frozen, 5); + Balances::remove_lock(ID_1, &1); + assert_eq!(Balances::account(&1).frozen, 4); + assert_eq!(System::consumers(&1), 1); + }); +} diff --git a/frame/balances/src/tests/dispatchable_tests.rs b/frame/balances/src/tests/dispatchable_tests.rs new file mode 100644 index 0000000000..76d0961e57 --- /dev/null +++ b/frame/balances/src/tests/dispatchable_tests.rs @@ -0,0 +1,224 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests regarding the functionality of the dispatchables/extrinsics. + +use super::*; +use frame_support::traits::tokens::Preservation::Expendable; +use fungible::{hold::Mutate as HoldMutate, Inspect, Mutate}; + +#[test] +fn default_indexing_on_new_accounts_should_not_work2() { + ExtBuilder::default() + .existential_deposit(10) + .monied(true) + .build_and_execute_with(|| { + // account 5 should not exist + // ext_deposit is 10, value is 9, not satisfies for ext_deposit + assert_noop!( + Balances::transfer_allow_death(Some(1).into(), 5, 9), + TokenError::BelowMinimum, + ); + assert_eq!(Balances::free_balance(1), 100); + }); +} + +#[test] +fn dust_account_removal_should_work() { + ExtBuilder::default() + .existential_deposit(100) + .monied(true) + .build_and_execute_with(|| { + System::inc_account_nonce(&2); + assert_eq!(System::account_nonce(&2), 1); + assert_eq!(Balances::total_balance(&2), 2000); + // index 1 (account 2) becomes zombie + assert_ok!(Balances::transfer_allow_death(Some(2).into(), 5, 1901)); + assert_eq!(Balances::total_balance(&2), 0); + assert_eq!(Balances::total_balance(&5), 1901); + assert_eq!(System::account_nonce(&2), 0); + }); +} + +#[test] +fn balance_transfer_works() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::mint_into(&1, 111); + assert_ok!(Balances::transfer_allow_death(Some(1).into(), 2, 69)); + assert_eq!(Balances::total_balance(&1), 42); + assert_eq!(Balances::total_balance(&2), 69); + }); +} + +#[test] +fn force_transfer_works() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::mint_into(&1, 111); + assert_noop!(Balances::force_transfer(Some(2).into(), 1, 2, 69), BadOrigin,); + assert_ok!(Balances::force_transfer(RawOrigin::Root.into(), 1, 2, 69)); + assert_eq!(Balances::total_balance(&1), 42); + assert_eq!(Balances::total_balance(&2), 69); + }); +} + +#[test] +fn balance_transfer_when_on_hold_should_not_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::mint_into(&1, 111); + assert_ok!(Balances::hold(&TestId::Foo, &1, 69)); + assert_noop!( + Balances::transfer_allow_death(Some(1).into(), 2, 69), + TokenError::FundsUnavailable, + ); + }); +} + +#[test] +fn transfer_keep_alive_works() { + ExtBuilder::default().existential_deposit(1).build_and_execute_with(|| { + let _ = Balances::mint_into(&1, 100); + assert_noop!( + Balances::transfer_keep_alive(Some(1).into(), 2, 100), + TokenError::NotExpendable + ); + assert_eq!(Balances::total_balance(&1), 100); + assert_eq!(Balances::total_balance(&2), 0); + }); +} + +#[test] +fn transfer_keep_alive_all_free_succeed() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 1, 300)); + assert_ok!(Balances::hold(&TestId::Foo, &1, 100)); + assert_ok!(Balances::transfer_keep_alive(Some(1).into(), 2, 100)); + assert_eq!(Balances::total_balance(&1), 200); + assert_eq!(Balances::total_balance(&2), 100); + }); +} + +#[test] +fn transfer_all_works_1() { + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + // setup + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 1, 200)); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 2, 0)); + // transfer all and allow death + assert_ok!(Balances::transfer_all(Some(1).into(), 2, false)); + assert_eq!(Balances::total_balance(&1), 0); + assert_eq!(Balances::total_balance(&2), 200); + }); +} + +#[test] +fn transfer_all_works_2() { + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + // setup + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 1, 200)); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 2, 0)); + // transfer all and keep alive + assert_ok!(Balances::transfer_all(Some(1).into(), 2, true)); + assert_eq!(Balances::total_balance(&1), 100); + assert_eq!(Balances::total_balance(&2), 100); + }); +} + +#[test] +fn transfer_all_works_3() { + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + // setup + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 1, 210)); + assert_ok!(Balances::hold(&TestId::Foo, &1, 10)); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 2, 0)); + // transfer all and allow death w/ reserved + assert_ok!(Balances::transfer_all(Some(1).into(), 2, false)); + assert_eq!(Balances::total_balance(&1), 110); + assert_eq!(Balances::total_balance(&2), 100); + }); +} + +#[test] +fn transfer_all_works_4() { + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + // setup + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 1, 210)); + assert_ok!(Balances::hold(&TestId::Foo, &1, 10)); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 2, 0)); + // transfer all and keep alive w/ reserved + assert_ok!(Balances::transfer_all(Some(1).into(), 2, true)); + assert_eq!(Balances::total_balance(&1), 110); + assert_eq!(Balances::total_balance(&2), 100); + }); +} + +#[test] +fn set_balance_handles_killing_account() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::mint_into(&1, 111); + assert_ok!(frame_system::Pallet::::inc_consumers(&1)); + assert_noop!( + Balances::force_set_balance(RuntimeOrigin::root(), 1, 0), + DispatchError::ConsumerRemaining, + ); + }); +} + +#[test] +fn set_balance_handles_total_issuance() { + ExtBuilder::default().build_and_execute_with(|| { + let old_total_issuance = Balances::total_issuance(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 1337, 69)); + assert_eq!(Balances::total_issuance(), old_total_issuance + 69); + assert_eq!(Balances::total_balance(&1337), 69); + assert_eq!(Balances::free_balance(&1337), 69); + }); +} + +#[test] +fn upgrade_accounts_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + System::inc_providers(&7); + assert_ok!(::AccountStore::try_mutate_exists( + &7, + |a| -> DispatchResult { + *a = Some(AccountData { + free: 5, + reserved: 5, + frozen: Zero::zero(), + flags: crate::types::ExtraFlags::old_logic(), + }); + Ok(()) + } + )); + assert!(!Balances::account(&7).flags.is_new_logic()); + assert_eq!(System::providers(&7), 1); + assert_eq!(System::consumers(&7), 0); + assert_ok!(Balances::upgrade_accounts(Some(1).into(), vec![7])); + assert!(Balances::account(&7).flags.is_new_logic()); + assert_eq!(System::providers(&7), 1); + assert_eq!(System::consumers(&7), 1); + + >::unreserve(&7, 5); + assert_ok!(>::transfer(&7, &1, 10, Expendable)); + assert_eq!(Balances::total_balance(&7), 0); + assert_eq!(System::providers(&7), 0); + assert_eq!(System::consumers(&7), 0); + }); +} diff --git a/frame/balances/src/tests/fungible_tests.rs b/frame/balances/src/tests/fungible_tests.rs new file mode 100644 index 0000000000..1280868853 --- /dev/null +++ b/frame/balances/src/tests/fungible_tests.rs @@ -0,0 +1,399 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests regarding the functionality of the `fungible` trait set implementations. + +use super::*; +use frame_support::traits::tokens::{ + Fortitude::{Force, Polite}, + Precision::{BestEffort, Exact}, + Preservation::{Expendable, Preserve, Protect}, + Restriction::Free, +}; +use fungible::{Inspect, InspectFreeze, InspectHold, Mutate, MutateFreeze, MutateHold, Unbalanced}; + +#[test] +fn inspect_trait_reducible_balance_basic_works() { + ExtBuilder::default().existential_deposit(10).build_and_execute_with(|| { + Balances::set_balance(&1, 100); + assert_eq!(Balances::reducible_balance(&1, Expendable, Polite), 100); + assert_eq!(Balances::reducible_balance(&1, Protect, Polite), 90); + assert_eq!(Balances::reducible_balance(&1, Preserve, Polite), 90); + assert_eq!(Balances::reducible_balance(&1, Expendable, Force), 100); + assert_eq!(Balances::reducible_balance(&1, Protect, Force), 90); + assert_eq!(Balances::reducible_balance(&1, Preserve, Force), 90); + }); +} + +#[test] +fn inspect_trait_reducible_balance_other_provide_works() { + ExtBuilder::default().existential_deposit(10).build_and_execute_with(|| { + Balances::set_balance(&1, 100); + System::inc_providers(&1); + assert_eq!(Balances::reducible_balance(&1, Expendable, Polite), 100); + assert_eq!(Balances::reducible_balance(&1, Protect, Polite), 100); + assert_eq!(Balances::reducible_balance(&1, Preserve, Polite), 90); + assert_eq!(Balances::reducible_balance(&1, Expendable, Force), 100); + assert_eq!(Balances::reducible_balance(&1, Protect, Force), 100); + assert_eq!(Balances::reducible_balance(&1, Preserve, Force), 90); + }); +} + +#[test] +fn inspect_trait_reducible_balance_frozen_works() { + ExtBuilder::default().existential_deposit(10).build_and_execute_with(|| { + Balances::set_balance(&1, 100); + assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 50)); + assert_eq!(Balances::reducible_balance(&1, Expendable, Polite), 50); + assert_eq!(Balances::reducible_balance(&1, Protect, Polite), 50); + assert_eq!(Balances::reducible_balance(&1, Preserve, Polite), 50); + assert_eq!(Balances::reducible_balance(&1, Expendable, Force), 90); + assert_eq!(Balances::reducible_balance(&1, Protect, Force), 90); + assert_eq!(Balances::reducible_balance(&1, Preserve, Force), 90); + }); +} + +#[test] +fn unbalanced_trait_set_balance_works() { + ExtBuilder::default().build_and_execute_with(|| { + assert_eq!(>::balance(&1337), 0); + assert_ok!(Balances::write_balance(&1337, 100)); + assert_eq!(>::balance(&1337), 100); + + assert_ok!(>::hold(&TestId::Foo, &1337, 60)); + assert_eq!(>::balance(&1337), 40); + assert_eq!(>::total_balance_on_hold(&1337), 60); + assert_eq!( + >::balance_on_hold(&TestId::Foo, &1337), + 60 + ); + + assert_noop!(Balances::write_balance(&1337, 0), Error::::InsufficientBalance); + + assert_ok!(Balances::write_balance(&1337, 1)); + assert_eq!(>::balance(&1337), 1); + assert_eq!( + >::balance_on_hold(&TestId::Foo, &1337), + 60 + ); + + assert_ok!(>::release(&TestId::Foo, &1337, 60, Exact)); + assert_eq!(>::balance_on_hold(&TestId::Foo, &1337), 0); + assert_eq!(>::total_balance_on_hold(&1337), 0); + }); +} + +#[test] +fn unbalanced_trait_set_total_issuance_works() { + ExtBuilder::default().build_and_execute_with(|| { + assert_eq!(>::total_issuance(), 0); + Balances::set_total_issuance(100); + assert_eq!(>::total_issuance(), 100); + }); +} + +#[test] +fn unbalanced_trait_decrease_balance_simple_works() { + ExtBuilder::default().build_and_execute_with(|| { + // An Account that starts at 100 + assert_ok!(Balances::write_balance(&1337, 100)); + assert_eq!(>::balance(&1337), 100); + // and reserves 50 + assert_ok!(>::hold(&TestId::Foo, &1337, 50)); + assert_eq!(>::balance(&1337), 50); + // and is decreased by 20 + assert_ok!(Balances::decrease_balance(&1337, 20, Exact, Expendable, Polite)); + assert_eq!(>::balance(&1337), 30); + }); +} + +#[test] +fn unbalanced_trait_decrease_balance_works_1() { + ExtBuilder::default().build_and_execute_with(|| { + assert_ok!(Balances::write_balance(&1337, 100)); + assert_eq!(>::balance(&1337), 100); + + assert_noop!( + Balances::decrease_balance(&1337, 101, Exact, Expendable, Polite), + TokenError::FundsUnavailable + ); + assert_eq!(Balances::decrease_balance(&1337, 100, Exact, Expendable, Polite), Ok(100)); + assert_eq!(>::balance(&1337), 0); + }); +} + +#[test] +fn unbalanced_trait_decrease_balance_works_2() { + ExtBuilder::default().build_and_execute_with(|| { + // free: 40, reserved: 60 + assert_ok!(Balances::write_balance(&1337, 100)); + assert_ok!(Balances::hold(&TestId::Foo, &1337, 60)); + assert_eq!(>::balance(&1337), 40); + assert_eq!(Balances::total_balance_on_hold(&1337), 60); + assert_noop!( + Balances::decrease_balance(&1337, 40, Exact, Expendable, Polite), + Error::::InsufficientBalance + ); + assert_eq!(Balances::decrease_balance(&1337, 39, Exact, Expendable, Polite), Ok(39)); + assert_eq!(>::balance(&1337), 1); + assert_eq!(Balances::total_balance_on_hold(&1337), 60); + }); +} + +#[test] +fn unbalanced_trait_decrease_balance_at_most_works_1() { + ExtBuilder::default().build_and_execute_with(|| { + assert_ok!(Balances::write_balance(&1337, 100)); + assert_eq!(>::balance(&1337), 100); + + assert_eq!(Balances::decrease_balance(&1337, 101, BestEffort, Expendable, Polite), Ok(100)); + assert_eq!(>::balance(&1337), 0); + }); +} + +#[test] +fn unbalanced_trait_decrease_balance_at_most_works_2() { + ExtBuilder::default().build_and_execute_with(|| { + assert_ok!(Balances::write_balance(&1337, 99)); + assert_eq!(Balances::decrease_balance(&1337, 99, BestEffort, Expendable, Polite), Ok(99)); + assert_eq!(>::balance(&1337), 0); + }); +} + +#[test] +fn unbalanced_trait_decrease_balance_at_most_works_3() { + ExtBuilder::default().build_and_execute_with(|| { + // free: 40, reserved: 60 + assert_ok!(Balances::write_balance(&1337, 100)); + assert_ok!(Balances::hold(&TestId::Foo, &1337, 60)); + assert_eq!(Balances::free_balance(1337), 40); + assert_eq!(Balances::total_balance_on_hold(&1337), 60); + assert_eq!(Balances::decrease_balance(&1337, 0, BestEffort, Expendable, Polite), Ok(0)); + assert_eq!(Balances::free_balance(1337), 40); + assert_eq!(Balances::total_balance_on_hold(&1337), 60); + assert_eq!(Balances::decrease_balance(&1337, 10, BestEffort, Expendable, Polite), Ok(10)); + assert_eq!(Balances::free_balance(1337), 30); + assert_eq!(Balances::decrease_balance(&1337, 200, BestEffort, Expendable, Polite), Ok(29)); + assert_eq!(>::balance(&1337), 1); + assert_eq!(Balances::free_balance(1337), 1); + assert_eq!(Balances::total_balance_on_hold(&1337), 60); + }); +} + +#[test] +fn unbalanced_trait_increase_balance_works() { + ExtBuilder::default().build_and_execute_with(|| { + assert_noop!(Balances::increase_balance(&1337, 0, Exact), TokenError::BelowMinimum); + assert_eq!(Balances::increase_balance(&1337, 1, Exact), Ok(1)); + assert_noop!(Balances::increase_balance(&1337, u64::MAX, Exact), ArithmeticError::Overflow); + }); +} + +#[test] +fn unbalanced_trait_increase_balance_at_most_works() { + ExtBuilder::default().build_and_execute_with(|| { + assert_eq!(Balances::increase_balance(&1337, 0, BestEffort), Ok(0)); + assert_eq!(Balances::increase_balance(&1337, 1, BestEffort), Ok(1)); + assert_eq!(Balances::increase_balance(&1337, u64::MAX, BestEffort), Ok(u64::MAX - 1)); + }); +} + +#[test] +fn freezing_and_holds_should_overlap() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 10)); + assert_ok!(Balances::hold(&TestId::Foo, &1, 9)); + assert_eq!(Balances::account(&1).free, 1); + assert_eq!(System::consumers(&1), 1); + assert_eq!(Balances::account(&1).free, 1); + assert_eq!(Balances::account(&1).frozen, 10); + assert_eq!(Balances::account(&1).reserved, 9); + assert_eq!(Balances::total_balance_on_hold(&1), 9); + }); +} + +#[test] +fn frozen_hold_balance_cannot_be_moved_without_force() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 10)); + assert_ok!(Balances::hold(&TestId::Foo, &1, 9)); + assert_eq!(Balances::reducible_total_balance_on_hold(&1, Force), 9); + assert_eq!(Balances::reducible_total_balance_on_hold(&1, Polite), 0); + let e = TokenError::Frozen; + assert_noop!( + Balances::transfer_on_hold(&TestId::Foo, &1, &2, 1, Exact, Free, Polite), + e + ); + assert_ok!(Balances::transfer_on_hold(&TestId::Foo, &1, &2, 1, Exact, Free, Force)); + }); +} + +#[test] +fn frozen_hold_balance_best_effort_transfer_works() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 5)); + assert_ok!(Balances::hold(&TestId::Foo, &1, 9)); + assert_eq!(Balances::reducible_total_balance_on_hold(&1, Force), 9); + assert_eq!(Balances::reducible_total_balance_on_hold(&1, Polite), 5); + assert_ok!(Balances::transfer_on_hold( + &TestId::Foo, + &1, + &2, + 10, + BestEffort, + Free, + Polite + )); + assert_eq!(Balances::total_balance(&1), 5); + assert_eq!(Balances::total_balance(&2), 25); + }); +} + +#[test] +fn partial_freezing_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 5)); + assert_eq!(System::consumers(&1), 1); + assert_ok!(>::transfer(&1, &2, 5, Expendable)); + assert_noop!( + >::transfer(&1, &2, 1, Expendable), + TokenError::Frozen + ); + }); +} + +#[test] +fn thaw_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &1, u64::MAX)); + assert_ok!(Balances::thaw(&TestId::Foo, &1)); + assert_eq!(System::consumers(&1), 0); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &1), 0); + assert_eq!(Balances::account(&1).frozen, 0); + assert_ok!(>::transfer(&1, &2, 10, Expendable)); + }); +} + +#[test] +fn set_freeze_zero_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &1, u64::MAX)); + assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 0)); + assert_eq!(System::consumers(&1), 0); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &1), 0); + assert_eq!(Balances::account(&1).frozen, 0); + assert_ok!(>::transfer(&1, &2, 10, Expendable)); + }); +} + +#[test] +fn set_freeze_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &1, u64::MAX)); + assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 5)); + assert_ok!(>::transfer(&1, &2, 5, Expendable)); + assert_noop!( + >::transfer(&1, &2, 1, Expendable), + TokenError::Frozen + ); + }); +} + +#[test] +fn extend_freeze_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 5)); + assert_ok!(Balances::extend_freeze(&TestId::Foo, &1, 10)); + assert_eq!(Balances::account(&1).frozen, 10); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &1), 10); + assert_noop!( + >::transfer(&1, &2, 1, Expendable), + TokenError::Frozen + ); + }); +} + +#[test] +fn double_freezing_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &1, 5)); + assert_ok!(Balances::set_freeze(&TestId::Bar, &1, 5)); + assert_eq!(System::consumers(&1), 1); + assert_ok!(>::transfer(&1, &2, 5, Expendable)); + assert_noop!( + >::transfer(&1, &2, 1, Expendable), + TokenError::Frozen + ); + }); +} + +#[test] +fn can_hold_entire_balance_when_second_provider() { + ExtBuilder::default() + .existential_deposit(1) + .monied(false) + .build_and_execute_with(|| { + >::set_balance(&1, 100); + assert_noop!(Balances::hold(&TestId::Foo, &1, 100), TokenError::FundsUnavailable); + System::inc_providers(&1); + assert_eq!(System::providers(&1), 2); + assert_ok!(Balances::hold(&TestId::Foo, &1, 100)); + assert_eq!(System::providers(&1), 1); + assert_noop!(System::dec_providers(&1), DispatchError::ConsumerRemaining); + }); +} + +#[test] +fn unholding_frees_hold_slot() { + ExtBuilder::default() + .existential_deposit(1) + .monied(false) + .build_and_execute_with(|| { + >::set_balance(&1, 100); + assert_ok!(Balances::hold(&TestId::Foo, &1, 10)); + assert_ok!(Balances::hold(&TestId::Bar, &1, 10)); + assert_ok!(Balances::release(&TestId::Foo, &1, 10, Exact)); + assert_ok!(Balances::hold(&TestId::Baz, &1, 10)); + }); +} diff --git a/frame/balances/src/tests/mod.rs b/frame/balances/src/tests/mod.rs new file mode 100644 index 0000000000..c4a8a631ca --- /dev/null +++ b/frame/balances/src/tests/mod.rs @@ -0,0 +1,296 @@ +// This file is part of Substrate. + +// Copyright (C) 2018-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests. + +#![cfg(test)] + +use crate::{self as pallet_balances, AccountData, Config, CreditOf, Error, Pallet}; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{ + assert_err, assert_noop, assert_ok, assert_storage_noop, + dispatch::DispatchInfo, + parameter_types, + traits::{ + tokens::fungible, ConstU32, ConstU64, ConstU8, Imbalance as ImbalanceT, OnUnbalanced, + StorageMapShim, StoredMap, + }, + weights::{IdentityFee, Weight}, + RuntimeDebug, +}; +use frame_system::{self as system, RawOrigin}; +use pallet_transaction_payment::{ChargeTransactionPayment, CurrencyAdapter, Multiplier}; +use scale_info::TypeInfo; +use sp_core::H256; +use sp_io; +use sp_runtime::{ + testing::Header, + traits::{BadOrigin, IdentityLookup, SignedExtension, Zero}, + ArithmeticError, DispatchError, DispatchResult, FixedPointNumber, TokenError, +}; + +mod currency_tests; +mod dispatchable_tests; +mod fungible_tests; +mod reentrancy_tests; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +#[derive( + Encode, + Decode, + Copy, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + MaxEncodedLen, + TypeInfo, + RuntimeDebug, +)] +pub enum TestId { + Foo, + Bar, + Baz, +} + +frame_support::construct_runtime!( + pub struct Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + TransactionPayment: pallet_transaction_payment::{Pallet, Storage, Event}, + } +); + +parameter_types! { + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max( + frame_support::weights::Weight::from_parts(1024, u64::MAX), + ); + pub static ExistentialDeposit: u64 = 0; +} +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = BlockWeights; + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type BlockNumber = u64; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = super::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_transaction_payment::Config for Test { + type RuntimeEvent = RuntimeEvent; + type OnChargeTransaction = CurrencyAdapter, ()>; + type OperationalFeeMultiplier = ConstU8<5>; + type WeightToFee = IdentityFee; + type LengthToFee = IdentityFee; + type FeeMultiplierUpdate = (); +} + +impl Config for Test { + type Balance = u64; + type DustRemoval = DustTrap; + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = TestAccountStore; + type MaxLocks = ConstU32<50>; + type MaxReserves = ConstU32<2>; + type ReserveIdentifier = TestId; + type WeightInfo = (); + type HoldIdentifier = TestId; + type FreezeIdentifier = TestId; + type MaxFreezes = ConstU32<2>; + type MaxHolds = ConstU32<2>; +} + +#[derive(Clone)] +pub struct ExtBuilder { + existential_deposit: u64, + monied: bool, + dust_trap: Option, +} +impl Default for ExtBuilder { + fn default() -> Self { + Self { existential_deposit: 1, monied: false, dust_trap: None } + } +} +impl ExtBuilder { + pub fn existential_deposit(mut self, existential_deposit: u64) -> Self { + self.existential_deposit = existential_deposit; + self + } + pub fn monied(mut self, monied: bool) -> Self { + self.monied = monied; + if self.existential_deposit == 0 { + self.existential_deposit = 1; + } + self + } + pub fn dust_trap(mut self, account: u64) -> Self { + self.dust_trap = Some(account); + self + } + pub fn set_associated_consts(&self) { + DUST_TRAP_TARGET.with(|v| v.replace(self.dust_trap)); + EXISTENTIAL_DEPOSIT.with(|v| v.replace(self.existential_deposit)); + } + pub fn build(self) -> sp_io::TestExternalities { + self.set_associated_consts(); + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + pallet_balances::GenesisConfig:: { + balances: if self.monied { + vec![ + (1, 10 * self.existential_deposit), + (2, 20 * self.existential_deposit), + (3, 30 * self.existential_deposit), + (4, 40 * self.existential_deposit), + (12, 10 * self.existential_deposit), + ] + } else { + vec![] + }, + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } + pub fn build_and_execute_with(self, f: impl Fn()) { + let other = self.clone(); + UseSystem::set(false); + other.build().execute_with(|| f()); + UseSystem::set(true); + self.build().execute_with(|| f()); + } +} + +parameter_types! { + static DustTrapTarget: Option = None; +} + +pub struct DustTrap; + +impl OnUnbalanced> for DustTrap { + fn on_nonzero_unbalanced(amount: CreditOf) { + match DustTrapTarget::get() { + None => drop(amount), + Some(a) => { + let result = >::resolve(&a, amount); + debug_assert!(result.is_ok()); + }, + } + } +} + +parameter_types! { + pub static UseSystem: bool = false; +} + +type BalancesAccountStore = StorageMapShim, u64, super::AccountData>; +type SystemAccountStore = frame_system::Pallet; + +pub struct TestAccountStore; +impl StoredMap> for TestAccountStore { + fn get(k: &u64) -> super::AccountData { + if UseSystem::get() { + >::get(k) + } else { + >::get(k) + } + } + fn try_mutate_exists>( + k: &u64, + f: impl FnOnce(&mut Option>) -> Result, + ) -> Result { + if UseSystem::get() { + >::try_mutate_exists(k, f) + } else { + >::try_mutate_exists(k, f) + } + } + fn mutate( + k: &u64, + f: impl FnOnce(&mut super::AccountData) -> R, + ) -> Result { + if UseSystem::get() { + >::mutate(k, f) + } else { + >::mutate(k, f) + } + } + fn mutate_exists( + k: &u64, + f: impl FnOnce(&mut Option>) -> R, + ) -> Result { + if UseSystem::get() { + >::mutate_exists(k, f) + } else { + >::mutate_exists(k, f) + } + } + fn insert(k: &u64, t: super::AccountData) -> Result<(), DispatchError> { + if UseSystem::get() { + >::insert(k, t) + } else { + >::insert(k, t) + } + } + fn remove(k: &u64) -> Result<(), DispatchError> { + if UseSystem::get() { + >::remove(k) + } else { + >::remove(k) + } + } +} + +pub fn events() -> Vec { + let evt = System::events().into_iter().map(|evt| evt.event).collect::>(); + System::reset_events(); + evt +} + +/// create a transaction info struct from weight. Handy to avoid building the whole struct. +pub fn info_from_weight(w: Weight) -> DispatchInfo { + DispatchInfo { weight: w, ..Default::default() } +} diff --git a/frame/balances/src/tests/reentrancy_tests.rs b/frame/balances/src/tests/reentrancy_tests.rs new file mode 100644 index 0000000000..e97bf2ed2b --- /dev/null +++ b/frame/balances/src/tests/reentrancy_tests.rs @@ -0,0 +1,195 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests regarding the reentrancy functionality. + +use super::*; +use frame_support::traits::tokens::{ + Fortitude::Force, + Precision::BestEffort, + Preservation::{Expendable, Protect}, +}; +use fungible::Balanced; + +#[test] +fn transfer_dust_removal_tst1_should_work() { + ExtBuilder::default() + .existential_deposit(100) + .dust_trap(1) + .build_and_execute_with(|| { + // Verification of reentrancy in dust removal + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 1, 1000)); + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 2, 500)); + + // In this transaction, account 2 free balance + // drops below existential balance + // and dust balance is removed from account 2 + assert_ok!(Balances::transfer_allow_death(RawOrigin::Signed(2).into(), 3, 450)); + + // As expected dust balance is removed. + assert_eq!(Balances::free_balance(&2), 0); + + // As expected beneficiary account 3 + // received the transfered fund. + assert_eq!(Balances::free_balance(&3), 450); + + // Dust balance is deposited to account 1 + // during the process of dust removal. + assert_eq!(Balances::free_balance(&1), 1050); + + // Verify the events + assert_eq!(System::events().len(), 12); + + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer { + from: 2, + to: 3, + amount: 450, + })); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost { + account: 2, + amount: 50, + })); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit { + who: 1, + amount: 50, + })); + }); +} + +#[test] +fn transfer_dust_removal_tst2_should_work() { + ExtBuilder::default() + .existential_deposit(100) + .dust_trap(1) + .build_and_execute_with(|| { + // Verification of reentrancy in dust removal + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 1, 1000)); + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 2, 500)); + + // In this transaction, account 2 free balance + // drops below existential balance + // and dust balance is removed from account 2 + assert_ok!(Balances::transfer_allow_death(RawOrigin::Signed(2).into(), 1, 450)); + + // As expected dust balance is removed. + assert_eq!(Balances::free_balance(&2), 0); + + // Dust balance is deposited to account 1 + // during the process of dust removal. + assert_eq!(Balances::free_balance(&1), 1500); + + // Verify the events + assert_eq!(System::events().len(), 10); + + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer { + from: 2, + to: 1, + amount: 450, + })); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost { + account: 2, + amount: 50, + })); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit { + who: 1, + amount: 50, + })); + }); +} + +#[test] +fn repatriating_reserved_balance_dust_removal_should_work() { + ExtBuilder::default() + .existential_deposit(100) + .dust_trap(1) + .build_and_execute_with(|| { + // Verification of reentrancy in dust removal + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 1, 1000)); + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 2, 500)); + + // Reserve a value on account 2, + // Such that free balance is lower than + // Exestintial deposit. + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(2), 1, 450)); + + // Since free balance of account 2 is lower than + // existential deposit, dust amount is + // removed from the account 2 + assert_eq!(Balances::reserved_balance(2), 0); + assert_eq!(Balances::free_balance(2), 0); + + // account 1 is credited with reserved amount + // together with dust balance during dust + // removal. + assert_eq!(Balances::reserved_balance(1), 0); + assert_eq!(Balances::free_balance(1), 1500); + + // Verify the events + assert_eq!(System::events().len(), 10); + + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer { + from: 2, + to: 1, + amount: 450, + })); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost { + account: 2, + amount: 50, + })); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit { + who: 1, + amount: 50, + })); + }); +} + +#[test] +fn emit_events_with_no_existential_deposit_suicide_with_dust() { + ExtBuilder::default().existential_deposit(2).build_and_execute_with(|| { + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), 1, 100)); + + assert_eq!( + events(), + [ + RuntimeEvent::System(system::Event::NewAccount { account: 1 }), + RuntimeEvent::Balances(crate::Event::Endowed { account: 1, free_balance: 100 }), + RuntimeEvent::Balances(crate::Event::BalanceSet { who: 1, free: 100 }), + ] + ); + + let res = Balances::withdraw(&1, 98, BestEffort, Protect, Force); + assert_eq!(res.unwrap().peek(), 98); + + // no events + assert_eq!( + events(), + [RuntimeEvent::Balances(crate::Event::Withdraw { who: 1, amount: 98 })] + ); + + let res = Balances::withdraw(&1, 1, BestEffort, Expendable, Force); + assert_eq!(res.unwrap().peek(), 1); + + assert_eq!( + events(), + [ + RuntimeEvent::System(system::Event::KilledAccount { account: 1 }), + RuntimeEvent::Balances(crate::Event::DustLost { account: 1, amount: 1 }), + RuntimeEvent::Balances(crate::Event::Withdraw { who: 1, amount: 1 }) + ] + ); + }); +} diff --git a/frame/balances/src/tests_composite.rs b/frame/balances/src/tests_composite.rs deleted file mode 100644 index 765ab194d2..0000000000 --- a/frame/balances/src/tests_composite.rs +++ /dev/null @@ -1,149 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Test utilities - -#![cfg(test)] - -use crate::{self as pallet_balances, decl_tests, Config, Pallet}; -use frame_support::{ - dispatch::DispatchInfo, - parameter_types, - traits::{ConstU32, ConstU64, ConstU8}, - weights::{IdentityFee, Weight}, -}; -use pallet_transaction_payment::CurrencyAdapter; -use sp_core::H256; -use sp_io; -use sp_runtime::{testing::Header, traits::IdentityLookup}; -type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; -type Block = frame_system::mocking::MockBlock; - -frame_support::construct_runtime!( - pub enum Test where - Block = Block, - NodeBlock = Block, - UncheckedExtrinsic = UncheckedExtrinsic, - { - System: frame_system::{Pallet, Call, Config, Storage, Event}, - Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, - TransactionPayment: pallet_transaction_payment::{Pallet, Storage, Event}, - } -); - -parameter_types! { - pub BlockWeights: frame_system::limits::BlockWeights = - frame_system::limits::BlockWeights::simple_max( - frame_support::weights::Weight::from_parts(1024, u64::MAX), - ); - pub static ExistentialDeposit: u64 = 0; -} -impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = BlockWeights; - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type Index = u64; - type BlockNumber = u64; - type RuntimeCall = RuntimeCall; - type Hash = H256; - type Hashing = ::sp_runtime::traits::BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Header = Header; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = ConstU64<250>; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = super::AccountData; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = (); - type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; -} - -impl pallet_transaction_payment::Config for Test { - type RuntimeEvent = RuntimeEvent; - type OnChargeTransaction = CurrencyAdapter, ()>; - type OperationalFeeMultiplier = ConstU8<5>; - type WeightToFee = IdentityFee; - type LengthToFee = IdentityFee; - type FeeMultiplierUpdate = (); -} - -impl Config for Test { - type Balance = u64; - type DustRemoval = (); - type RuntimeEvent = RuntimeEvent; - type ExistentialDeposit = ExistentialDeposit; - type AccountStore = frame_system::Pallet; - type MaxLocks = (); - type MaxReserves = ConstU32<2>; - type ReserveIdentifier = [u8; 8]; - type WeightInfo = (); -} - -pub struct ExtBuilder { - existential_deposit: u64, - monied: bool, -} -impl Default for ExtBuilder { - fn default() -> Self { - Self { existential_deposit: 1, monied: false } - } -} -impl ExtBuilder { - pub fn existential_deposit(mut self, existential_deposit: u64) -> Self { - self.existential_deposit = existential_deposit; - self - } - pub fn monied(mut self, monied: bool) -> Self { - self.monied = monied; - self - } - pub fn set_associated_consts(&self) { - EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit); - } - pub fn build(self) -> sp_io::TestExternalities { - self.set_associated_consts(); - let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); - pallet_balances::GenesisConfig:: { - balances: if self.monied { - vec![ - (1, 10 * self.existential_deposit), - (2, 20 * self.existential_deposit), - (3, 30 * self.existential_deposit), - (4, 40 * self.existential_deposit), - (12, 10 * self.existential_deposit), - ] - } else { - vec![] - }, - } - .assimilate_storage(&mut t) - .unwrap(); - - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| System::set_block_number(1)); - ext - } -} - -decl_tests! { Test, ExtBuilder, EXISTENTIAL_DEPOSIT } diff --git a/frame/balances/src/tests_local.rs b/frame/balances/src/tests_local.rs deleted file mode 100644 index 929f77b540..0000000000 --- a/frame/balances/src/tests_local.rs +++ /dev/null @@ -1,191 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Test utilities - -#![cfg(test)] - -use crate::{self as pallet_balances, decl_tests, Config, Pallet}; -use frame_support::{ - dispatch::DispatchInfo, - parameter_types, - traits::{ConstU32, ConstU64, ConstU8, StorageMapShim}, - weights::{IdentityFee, Weight}, -}; -use pallet_transaction_payment::CurrencyAdapter; -use sp_core::H256; -use sp_io; -use sp_runtime::{testing::Header, traits::IdentityLookup}; - -type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; -type Block = frame_system::mocking::MockBlock; - -frame_support::construct_runtime!( - pub struct Test where - Block = Block, - NodeBlock = Block, - UncheckedExtrinsic = UncheckedExtrinsic, - { - System: frame_system::{Pallet, Call, Config, Storage, Event}, - Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, - TransactionPayment: pallet_transaction_payment::{Pallet, Storage, Event}, - } -); - -parameter_types! { - pub BlockWeights: frame_system::limits::BlockWeights = - frame_system::limits::BlockWeights::simple_max( - frame_support::weights::Weight::from_parts(1024, u64::MAX), - ); - pub static ExistentialDeposit: u64 = 0; -} -impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = BlockWeights; - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type Index = u64; - type BlockNumber = u64; - type RuntimeCall = RuntimeCall; - type Hash = H256; - type Hashing = ::sp_runtime::traits::BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Header = Header; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = ConstU64<250>; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = (); - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = (); - type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; -} - -impl pallet_transaction_payment::Config for Test { - type RuntimeEvent = RuntimeEvent; - type OnChargeTransaction = CurrencyAdapter, ()>; - type OperationalFeeMultiplier = ConstU8<5>; - type WeightToFee = IdentityFee; - type LengthToFee = IdentityFee; - type FeeMultiplierUpdate = (); -} - -impl Config for Test { - type Balance = u64; - type DustRemoval = (); - type RuntimeEvent = RuntimeEvent; - type ExistentialDeposit = ExistentialDeposit; - type AccountStore = - StorageMapShim, system::Provider, u64, super::AccountData>; - type MaxLocks = ConstU32<50>; - type MaxReserves = ConstU32<2>; - type ReserveIdentifier = [u8; 8]; - type WeightInfo = (); -} - -pub struct ExtBuilder { - existential_deposit: u64, - monied: bool, -} -impl Default for ExtBuilder { - fn default() -> Self { - Self { existential_deposit: 1, monied: false } - } -} -impl ExtBuilder { - pub fn existential_deposit(mut self, existential_deposit: u64) -> Self { - self.existential_deposit = existential_deposit; - self - } - pub fn monied(mut self, monied: bool) -> Self { - self.monied = monied; - if self.existential_deposit == 0 { - self.existential_deposit = 1; - } - self - } - pub fn set_associated_consts(&self) { - EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit); - } - pub fn build(self) -> sp_io::TestExternalities { - self.set_associated_consts(); - let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); - pallet_balances::GenesisConfig:: { - balances: if self.monied { - vec![ - (1, 10 * self.existential_deposit), - (2, 20 * self.existential_deposit), - (3, 30 * self.existential_deposit), - (4, 40 * self.existential_deposit), - (12, 10 * self.existential_deposit), - ] - } else { - vec![] - }, - } - .assimilate_storage(&mut t) - .unwrap(); - - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| System::set_block_number(1)); - ext - } -} - -decl_tests! { Test, ExtBuilder, EXISTENTIAL_DEPOSIT } - -#[test] -fn emit_events_with_no_existential_deposit_suicide_with_dust() { - ::default().existential_deposit(2).build().execute_with(|| { - assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 1, 100, 0)); - - assert_eq!( - events(), - [ - RuntimeEvent::System(system::Event::NewAccount { account: 1 }), - RuntimeEvent::Balances(crate::Event::Endowed { account: 1, free_balance: 100 }), - RuntimeEvent::Balances(crate::Event::BalanceSet { who: 1, free: 100, reserved: 0 }), - ] - ); - - let res = Balances::slash(&1, 98); - assert_eq!(res, (NegativeImbalance::new(98), 0)); - - // no events - assert_eq!( - events(), - [RuntimeEvent::Balances(crate::Event::Slashed { who: 1, amount: 98 })] - ); - - let res = Balances::slash(&1, 1); - assert_eq!(res, (NegativeImbalance::new(1), 0)); - - assert_eq!( - events(), - [ - RuntimeEvent::System(system::Event::KilledAccount { account: 1 }), - RuntimeEvent::Balances(crate::Event::DustLost { account: 1, amount: 1 }), - RuntimeEvent::Balances(crate::Event::Slashed { who: 1, amount: 1 }) - ] - ); - }); -} diff --git a/frame/balances/src/tests_reentrancy.rs b/frame/balances/src/tests_reentrancy.rs deleted file mode 100644 index 828dfa2370..0000000000 --- a/frame/balances/src/tests_reentrancy.rs +++ /dev/null @@ -1,264 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Test setup for potential reentracy and lost updates of nested mutations. - -#![cfg(test)] - -use crate::{self as pallet_balances, Config}; -use frame_support::{ - parameter_types, - traits::{ConstU32, ConstU64, StorageMapShim}, -}; -use sp_core::H256; -use sp_io; -use sp_runtime::{testing::Header, traits::IdentityLookup}; - -use crate::*; -use frame_support::{ - assert_ok, - traits::{Currency, ReservableCurrency}, -}; -use frame_system::RawOrigin; - -type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; -type Block = frame_system::mocking::MockBlock; - -frame_support::construct_runtime!( - pub enum Test where - Block = Block, - NodeBlock = Block, - UncheckedExtrinsic = UncheckedExtrinsic, - { - System: frame_system::{Pallet, Call, Config, Storage, Event}, - Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, - } -); - -parameter_types! { - pub BlockWeights: frame_system::limits::BlockWeights = - frame_system::limits::BlockWeights::simple_max( - frame_support::weights::Weight::from_parts(1024, u64::MAX), - ); - pub static ExistentialDeposit: u64 = 0; -} -impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = BlockWeights; - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type Index = u64; - type BlockNumber = u64; - type RuntimeCall = RuntimeCall; - type Hash = H256; - type Hashing = ::sp_runtime::traits::BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Header = Header; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = ConstU64<250>; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = (); - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = (); - type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; -} - -pub struct OnDustRemoval; -impl OnUnbalanced> for OnDustRemoval { - fn on_nonzero_unbalanced(amount: NegativeImbalance) { - assert_ok!(Balances::resolve_into_existing(&1, amount)); - } -} - -impl Config for Test { - type Balance = u64; - type DustRemoval = OnDustRemoval; - type RuntimeEvent = RuntimeEvent; - type ExistentialDeposit = ExistentialDeposit; - type AccountStore = - StorageMapShim, system::Provider, u64, super::AccountData>; - type MaxLocks = ConstU32<50>; - type MaxReserves = ConstU32<2>; - type ReserveIdentifier = [u8; 8]; - type WeightInfo = (); -} - -pub struct ExtBuilder { - existential_deposit: u64, -} -impl Default for ExtBuilder { - fn default() -> Self { - Self { existential_deposit: 1 } - } -} -impl ExtBuilder { - pub fn existential_deposit(mut self, existential_deposit: u64) -> Self { - self.existential_deposit = existential_deposit; - self - } - - pub fn set_associated_consts(&self) { - EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit); - } - - pub fn build(self) -> sp_io::TestExternalities { - self.set_associated_consts(); - let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); - pallet_balances::GenesisConfig:: { balances: vec![] } - .assimilate_storage(&mut t) - .unwrap(); - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| System::set_block_number(1)); - ext - } -} - -#[test] -fn transfer_dust_removal_tst1_should_work() { - ExtBuilder::default().existential_deposit(100).build().execute_with(|| { - // Verification of reentrancy in dust removal - assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 1, 1000, 0)); - assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 2, 500, 0)); - - // In this transaction, account 2 free balance - // drops below existential balance - // and dust balance is removed from account 2 - assert_ok!(Balances::transfer(RawOrigin::Signed(2).into(), 3, 450)); - - // As expected dust balance is removed. - assert_eq!(Balances::free_balance(&2), 0); - - // As expected beneficiary account 3 - // received the transfered fund. - assert_eq!(Balances::free_balance(&3), 450); - - // Dust balance is deposited to account 1 - // during the process of dust removal. - assert_eq!(Balances::free_balance(&1), 1050); - - // Verify the events - assert_eq!(System::events().len(), 12); - - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer { - from: 2, - to: 3, - amount: 450, - })); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost { - account: 2, - amount: 50, - })); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit { - who: 1, - amount: 50, - })); - }); -} - -#[test] -fn transfer_dust_removal_tst2_should_work() { - ExtBuilder::default().existential_deposit(100).build().execute_with(|| { - // Verification of reentrancy in dust removal - assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 1, 1000, 0)); - assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 2, 500, 0)); - - // In this transaction, account 2 free balance - // drops below existential balance - // and dust balance is removed from account 2 - assert_ok!(Balances::transfer(RawOrigin::Signed(2).into(), 1, 450)); - - // As expected dust balance is removed. - assert_eq!(Balances::free_balance(&2), 0); - - // Dust balance is deposited to account 1 - // during the process of dust removal. - assert_eq!(Balances::free_balance(&1), 1500); - - // Verify the events - assert_eq!(System::events().len(), 10); - - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer { - from: 2, - to: 1, - amount: 450, - })); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost { - account: 2, - amount: 50, - })); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit { - who: 1, - amount: 50, - })); - }); -} - -#[test] -fn repatriating_reserved_balance_dust_removal_should_work() { - ExtBuilder::default().existential_deposit(100).build().execute_with(|| { - // Verification of reentrancy in dust removal - assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 1, 1000, 0)); - assert_ok!(Balances::set_balance(RawOrigin::Root.into(), 2, 500, 0)); - - // Reserve a value on account 2, - // Such that free balance is lower than - // Exestintial deposit. - assert_ok!(Balances::reserve(&2, 450)); - - // Transfer of reserved fund from slashed account 2 to - // beneficiary account 1 - assert_ok!(Balances::repatriate_reserved(&2, &1, 450, Status::Free), 0); - - // Since free balance of account 2 is lower than - // existential deposit, dust amount is - // removed from the account 2 - assert_eq!(Balances::reserved_balance(2), 0); - assert_eq!(Balances::free_balance(2), 0); - - // account 1 is credited with reserved amount - // together with dust balance during dust - // removal. - assert_eq!(Balances::reserved_balance(1), 0); - assert_eq!(Balances::free_balance(1), 1500); - - // Verify the events - assert_eq!(System::events().len(), 11); - - System::assert_has_event(RuntimeEvent::Balances(crate::Event::ReserveRepatriated { - from: 2, - to: 1, - amount: 450, - destination_status: Status::Free, - })); - - System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost { - account: 2, - amount: 50, - })); - - System::assert_last_event(RuntimeEvent::Balances(crate::Event::Deposit { - who: 1, - amount: 50, - })); - }); -} diff --git a/frame/balances/src/types.rs b/frame/balances/src/types.rs new file mode 100644 index 0000000000..c96e1e44b4 --- /dev/null +++ b/frame/balances/src/types.rs @@ -0,0 +1,157 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Types used in the pallet. + +use crate::{Config, CreditOf, Event, Pallet}; +use codec::{Decode, Encode, MaxEncodedLen}; +use core::ops::BitOr; +use frame_support::{ + traits::{Imbalance, LockIdentifier, OnUnbalanced, WithdrawReasons}, + RuntimeDebug, +}; +use scale_info::TypeInfo; +use sp_runtime::Saturating; + +/// Simplified reasons for withdrawing balance. +#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub enum Reasons { + /// Paying system transaction fees. + Fee = 0, + /// Any reason other than paying system transaction fees. + Misc = 1, + /// Any reason at all. + All = 2, +} + +impl From for Reasons { + fn from(r: WithdrawReasons) -> Reasons { + if r == WithdrawReasons::TRANSACTION_PAYMENT { + Reasons::Fee + } else if r.contains(WithdrawReasons::TRANSACTION_PAYMENT) { + Reasons::All + } else { + Reasons::Misc + } + } +} + +impl BitOr for Reasons { + type Output = Reasons; + fn bitor(self, other: Reasons) -> Reasons { + if self == other { + return self + } + Reasons::All + } +} + +/// A single lock on a balance. There can be many of these on an account and they "overlap", so the +/// same balance is frozen by multiple locks. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct BalanceLock { + /// An identifier for this lock. Only one lock may be in existence for each identifier. + pub id: LockIdentifier, + /// The amount which the free balance may not drop below when this lock is in effect. + pub amount: Balance, + /// If true, then the lock remains in effect even for payment of transaction fees. + pub reasons: Reasons, +} + +/// Store named reserved balance. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct ReserveData { + /// The identifier for the named reserve. + pub id: ReserveIdentifier, + /// The amount of the named reserve. + pub amount: Balance, +} + +/// An identifier and balance. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct IdAmount { + /// An identifier for this item. + pub id: Id, + /// Some amount for this item. + pub amount: Balance, +} + +/// All balance information for an account. +#[derive(Encode, Decode, Clone, PartialEq, Eq, Default, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct AccountData { + /// Non-reserved part of the balance which the account holder may be able to control. + /// + /// This is the only balance that matters in terms of most operations on tokens. + pub free: Balance, + /// Balance which is has active holds on it and may not be used at all. + /// + /// This is the sum of all individual holds together with any sums still under the (deprecated) + /// reserves API. + pub reserved: Balance, + /// The amount that `free` may not drop below when reducing the balance, except for actions + /// where the account owner cannot reasonably benefit from thr balance reduction, such as + /// slashing. + pub frozen: Balance, + /// Extra information about this account. The MSB is a flag indicating whether the new ref- + /// counting logic is in place for this account. + pub flags: ExtraFlags, +} + +const IS_NEW_LOGIC: u128 = 0x80000000_00000000_00000000_00000000u128; + +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct ExtraFlags(u128); +impl Default for ExtraFlags { + fn default() -> Self { + Self(IS_NEW_LOGIC) + } +} +impl ExtraFlags { + pub fn old_logic() -> Self { + Self(0) + } + pub fn set_new_logic(&mut self) { + self.0 = self.0 | IS_NEW_LOGIC + } + pub fn is_new_logic(&self) -> bool { + (self.0 & IS_NEW_LOGIC) == IS_NEW_LOGIC + } +} + +impl AccountData { + pub fn usable(&self) -> Balance { + self.free.saturating_sub(self.frozen) + } + + /// The total balance in this account including any that is reserved and ignoring any frozen. + pub fn total(&self) -> Balance { + self.free.saturating_add(self.reserved) + } +} + +pub struct DustCleaner, I: 'static = ()>( + pub(crate) Option<(T::AccountId, CreditOf)>, +); + +impl, I: 'static> Drop for DustCleaner { + fn drop(&mut self) { + if let Some((who, dust)) = self.0.take() { + Pallet::::deposit_event(Event::DustLost { account: who, amount: dust.peek() }); + T::DustRemoval::on_unbalanced(dust); + } + } +} diff --git a/frame/balances/src/weights.rs b/frame/balances/src/weights.rs index e7fd539c06..a2a9f0019b 100644 --- a/frame/balances/src/weights.rs +++ b/frame/balances/src/weights.rs @@ -49,13 +49,14 @@ use sp_std::marker::PhantomData; /// Weight functions needed for pallet_balances. pub trait WeightInfo { - fn transfer() -> Weight; + fn transfer_allow_death() -> Weight; fn transfer_keep_alive() -> Weight; - fn set_balance_creating() -> Weight; - fn set_balance_killing() -> Weight; + fn force_set_balance_creating() -> Weight; + fn force_set_balance_killing() -> Weight; fn force_transfer() -> Weight; fn transfer_all() -> Weight; fn force_unreserve() -> Weight; + fn upgrade_accounts(u: u32, ) -> Weight; } /// Weights for pallet_balances using the Substrate node and recommended hardware. @@ -63,7 +64,7 @@ pub struct SubstrateWeight(PhantomData); impl WeightInfo for SubstrateWeight { /// Storage: System Account (r:1 w:1) /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - fn transfer() -> Weight { + fn transfer_allow_death() -> Weight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `3593` @@ -85,7 +86,7 @@ impl WeightInfo for SubstrateWeight { } /// Storage: System Account (r:1 w:1) /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - fn set_balance_creating() -> Weight { + fn force_set_balance_creating() -> Weight { // Proof Size summary in bytes: // Measured: `174` // Estimated: `3593` @@ -96,7 +97,7 @@ impl WeightInfo for SubstrateWeight { } /// Storage: System Account (r:1 w:1) /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - fn set_balance_killing() -> Weight { + fn force_set_balance_killing() -> Weight { // Proof Size summary in bytes: // Measured: `174` // Estimated: `3593` @@ -138,13 +139,28 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } + /// Storage: System Account (r:999 w:999) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// The range of component `u` is `[1, 1000]`. + fn upgrade_accounts(u: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + u * (135 ±0)` + // Estimated: `990 + u * (2603 ±0)` + // Minimum execution time: 19_851_000 picoseconds. + Weight::from_parts(20_099_000, 990) + // Standard Error: 15_586 + .saturating_add(Weight::from_parts(14_892_860, 0).saturating_mul(u.into())) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(u.into()))) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(u.into()))) + .saturating_add(Weight::from_parts(0, 2603).saturating_mul(u.into())) + } } // For backwards compatibility and tests impl WeightInfo for () { /// Storage: System Account (r:1 w:1) /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - fn transfer() -> Weight { + fn transfer_allow_death() -> Weight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `3593` @@ -166,7 +182,7 @@ impl WeightInfo for () { } /// Storage: System Account (r:1 w:1) /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - fn set_balance_creating() -> Weight { + fn force_set_balance_creating() -> Weight { // Proof Size summary in bytes: // Measured: `174` // Estimated: `3593` @@ -177,7 +193,7 @@ impl WeightInfo for () { } /// Storage: System Account (r:1 w:1) /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - fn set_balance_killing() -> Weight { + fn force_set_balance_killing() -> Weight { // Proof Size summary in bytes: // Measured: `174` // Estimated: `3593` @@ -219,4 +235,19 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } + /// Storage: System Account (r:999 w:999) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// The range of component `u` is `[1, 1000]`. + fn upgrade_accounts(u: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + u * (135 ±0)` + // Estimated: `990 + u * (2603 ±0)` + // Minimum execution time: 19_851_000 picoseconds. + Weight::from_parts(20_099_000, 990) + // Standard Error: 15_586 + .saturating_add(Weight::from_parts(14_892_860, 0).saturating_mul(u.into())) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(u.into()))) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(u.into()))) + .saturating_add(Weight::from_parts(0, 2603).saturating_mul(u.into())) + } } diff --git a/frame/beefy/src/mock.rs b/frame/beefy/src/mock.rs index c929662351..ceb95263e2 100644 --- a/frame/beefy/src/mock.rs +++ b/frame/beefy/src/mock.rs @@ -161,6 +161,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU128<1>; type AccountStore = System; type WeightInfo = (); + type HoldIdentifier = (); + type MaxHolds = (); + type FreezeIdentifier = (); + type MaxFreezes = (); } impl pallet_timestamp::Config for Test { diff --git a/frame/bounties/src/benchmarking.rs b/frame/bounties/src/benchmarking.rs index 04465a25eb..0675328c3d 100644 --- a/frame/bounties/src/benchmarking.rs +++ b/frame/bounties/src/benchmarking.rs @@ -57,9 +57,12 @@ fn setup_bounty, I: 'static>( let fee = value / 2u32.into(); let deposit = T::BountyDepositBase::get() + T::DataDepositPerByte::get() * T::MaximumReasonLength::get().into(); - let _ = T::Currency::make_free_balance_be(&caller, deposit); + let _ = T::Currency::make_free_balance_be(&caller, deposit + T::Currency::minimum_balance()); let curator = account("curator", u, SEED); - let _ = T::Currency::make_free_balance_be(&curator, fee / 2u32.into()); + let _ = T::Currency::make_free_balance_be( + &curator, + fee / 2u32.into() + T::Currency::minimum_balance(), + ); let reason = vec![0; d as usize]; (caller, curator, fee, value, reason) } diff --git a/frame/bounties/src/tests.rs b/frame/bounties/src/tests.rs index 27aa085852..ef3da75648 100644 --- a/frame/bounties/src/tests.rs +++ b/frame/bounties/src/tests.rs @@ -100,6 +100,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } parameter_types! { pub const ProposalBond: Permill = Permill::from_percent(5); @@ -754,7 +758,7 @@ fn award_and_claim_bounty_works() { System::set_block_number(5); >::on_initialize(5); - assert_ok!(Balances::transfer( + assert_ok!(Balances::transfer_allow_death( RuntimeOrigin::signed(0), Bounties::bounty_account_id(0), 10 @@ -832,7 +836,7 @@ fn cancel_and_refund() { System::set_block_number(2); >::on_initialize(2); - assert_ok!(Balances::transfer( + assert_ok!(Balances::transfer_allow_death( RuntimeOrigin::signed(0), Bounties::bounty_account_id(0), 10 diff --git a/frame/child-bounties/src/benchmarking.rs b/frame/child-bounties/src/benchmarking.rs index 0a83f7c394..e49d9c8361 100644 --- a/frame/child-bounties/src/benchmarking.rs +++ b/frame/child-bounties/src/benchmarking.rs @@ -63,9 +63,12 @@ fn setup_bounty( let fee = value / 2u32.into(); let deposit = T::BountyDepositBase::get() + T::DataDepositPerByte::get() * T::MaximumReasonLength::get().into(); - let _ = T::Currency::make_free_balance_be(&caller, deposit); + let _ = T::Currency::make_free_balance_be(&caller, deposit + T::Currency::minimum_balance()); let curator = account("curator", user, SEED); - let _ = T::Currency::make_free_balance_be(&curator, fee / 2u32.into()); + let _ = T::Currency::make_free_balance_be( + &curator, + fee / 2u32.into() + T::Currency::minimum_balance(), + ); let reason = vec![0; description as usize]; (caller, curator, fee, value, reason) } @@ -73,7 +76,10 @@ fn setup_bounty( fn setup_child_bounty(user: u32, description: u32) -> BenchmarkChildBounty { let (caller, curator, fee, value, reason) = setup_bounty::(user, description); let child_curator = account("child-curator", user, SEED); - let _ = T::Currency::make_free_balance_be(&child_curator, fee / 2u32.into()); + let _ = T::Currency::make_free_balance_be( + &child_curator, + fee / 2u32.into() + T::Currency::minimum_balance(), + ); let child_bounty_value = (value - fee) / 4u32.into(); let child_bounty_fee = child_bounty_value / 2u32.into(); diff --git a/frame/child-bounties/src/tests.rs b/frame/child-bounties/src/tests.rs index 0172468ec5..a936312aec 100644 --- a/frame/child-bounties/src/tests.rs +++ b/frame/child-bounties/src/tests.rs @@ -35,7 +35,7 @@ use sp_core::H256; use sp_runtime::{ testing::Header, traits::{BadOrigin, BlakeTwo256, IdentityLookup}, - Perbill, Permill, + Perbill, Permill, TokenError, }; use super::Event as ChildBountiesEvent; @@ -103,6 +103,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } parameter_types! { pub const ProposalBond: Permill = Permill::from_percent(5); @@ -248,7 +252,7 @@ fn add_child_bounty() { assert_noop!( ChildBounties::add_child_bounty(RuntimeOrigin::signed(4), 0, 50, b"12345-p1".to_vec()), - pallet_balances::Error::::KeepAlive, + TokenError::NotExpendable, ); assert_noop!( diff --git a/frame/contracts/src/benchmarking/mod.rs b/frame/contracts/src/benchmarking/mod.rs index 2fb033cf85..fb8e4ac4b3 100644 --- a/frame/contracts/src/benchmarking/mod.rs +++ b/frame/contracts/src/benchmarking/mod.rs @@ -798,15 +798,15 @@ benchmarks! { let instance = Contract::::new(code, vec![])?; let origin = RawOrigin::Signed(instance.caller.clone()); let deposit_account = instance.info()?.deposit_account().clone(); - assert_eq!(T::Currency::total_balance(&beneficiary), 0u32.into()); + assert_eq!(>::total_balance(&beneficiary), 0u32.into()); assert_eq!(T::Currency::free_balance(&instance.account_id), Pallet::::min_balance() * 2u32.into()); assert_ne!(T::Currency::free_balance(&deposit_account), 0u32.into()); }: call(origin, instance.addr.clone(), 0u32.into(), Weight::MAX, None, vec![]) verify { if r > 0 { - assert_eq!(T::Currency::total_balance(&instance.account_id), 0u32.into()); - assert_eq!(T::Currency::total_balance(&deposit_account), 0u32.into()); - assert_eq!(T::Currency::total_balance(&beneficiary), Pallet::::min_balance() * 2u32.into()); + assert_eq!(>::total_balance(&instance.account_id), 0u32.into()); + assert_eq!(>::total_balance(&deposit_account), 0u32.into()); + assert_eq!(>::total_balance(&beneficiary), Pallet::::min_balance() * 2u32.into()); } } @@ -1573,12 +1573,12 @@ benchmarks! { instance.set_balance(value * (r + 1).into()); let origin = RawOrigin::Signed(instance.caller.clone()); for account in &accounts { - assert_eq!(T::Currency::total_balance(account), 0u32.into()); + assert_eq!(>::total_balance(account), 0u32.into()); } }: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![]) verify { for account in &accounts { - assert_eq!(T::Currency::total_balance(account), value); + assert_eq!(>::total_balance(account), value); } } diff --git a/frame/contracts/src/exec.rs b/frame/contracts/src/exec.rs index bd63c524c6..6a053712e6 100644 --- a/frame/contracts/src/exec.rs +++ b/frame/contracts/src/exec.rs @@ -25,7 +25,10 @@ use frame_support::{ crypto::ecdsa::ECDSAExt, dispatch::{DispatchError, DispatchResult, DispatchResultWithPostInfo, Dispatchable}, storage::{with_transaction, TransactionOutcome}, - traits::{Contains, Currency, ExistenceRequirement, OriginTrait, Randomness, Time}, + traits::{ + tokens::{Fortitude::Polite, Preservation::Expendable}, + Contains, Currency, ExistenceRequirement, OriginTrait, Randomness, Time, + }, weights::Weight, Blake2_128Concat, BoundedVec, StorageHasher, }; @@ -1198,7 +1201,7 @@ where T::Currency::transfer( &frame.account_id, beneficiary, - T::Currency::reducible_balance(&frame.account_id, false), + T::Currency::reducible_balance(&frame.account_id, Expendable, Polite), ExistenceRequirement::AllowDeath, )?; info.queue_trie_for_deletion()?; @@ -2751,8 +2754,10 @@ mod tests { RuntimeCall::System(SysCall::remark_with_event { remark: b"Hello".to_vec() }); // transfers are disallowed by the `TestFiler` (see below) - let forbidden_call = - RuntimeCall::Balances(BalanceCall::transfer { dest: CHARLIE, value: 22 }); + let forbidden_call = RuntimeCall::Balances(BalanceCall::transfer_allow_death { + dest: CHARLIE, + value: 22, + }); // simple cases: direct call assert_err!( @@ -2772,7 +2777,7 @@ mod tests { }); TestFilter::set_filter(|call| match call { - RuntimeCall::Balances(pallet_balances::Call::transfer { .. }) => false, + RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { .. }) => false, _ => true, }); diff --git a/frame/contracts/src/lib.rs b/frame/contracts/src/lib.rs index 7aac94d111..d3369b6c29 100644 --- a/frame/contracts/src/lib.rs +++ b/frame/contracts/src/lib.rs @@ -187,7 +187,7 @@ pub mod pallet { type Randomness: Randomness; /// The currency in which fees are paid and contract balances are held. - type Currency: ReservableCurrency + type Currency: ReservableCurrency // TODO: Move to fungible traits + Inspect>; /// The overarching event type. diff --git a/frame/contracts/src/storage/meter.rs b/frame/contracts/src/storage/meter.rs index b4fc443fe7..ef3e01f6d6 100644 --- a/frame/contracts/src/storage/meter.rs +++ b/frame/contracts/src/storage/meter.rs @@ -25,7 +25,10 @@ use codec::Encode; use frame_support::{ dispatch::DispatchError, ensure, - traits::{tokens::WithdrawConsequence, Currency, ExistenceRequirement, Get}, + traits::{ + tokens::{Fortitude::Polite, Preservation::Protect, WithdrawConsequence}, + Currency, ExistenceRequirement, Get, + }, DefaultNoBound, RuntimeDebugNoBound, }; use pallet_contracts_primitives::StorageDeposit as Deposit; @@ -456,7 +459,7 @@ impl Ext for ReservingExt { // We are sending the `min_leftover` and the `min_balance` from the origin // account as part of a contract call. Hence origin needs to have those left over // as free balance after accounting for all deposits. - let max = T::Currency::reducible_balance(origin, true) + let max = T::Currency::reducible_balance(origin, Protect, Polite) .saturating_sub(min_leftover) .saturating_sub(Pallet::::min_balance()); let limit = limit.unwrap_or(max); diff --git a/frame/contracts/src/tests.rs b/frame/contracts/src/tests.rs index 30a39eebfc..185945b71e 100644 --- a/frame/contracts/src/tests.rs +++ b/frame/contracts/src/tests.rs @@ -37,7 +37,7 @@ use frame_support::{ storage::child, traits::{ ConstU32, ConstU64, Contains, Currency, ExistenceRequirement, Get, LockableCurrency, - OnIdle, OnInitialize, ReservableCurrency, WithdrawReasons, + OnIdle, OnInitialize, WithdrawReasons, }, weights::{constants::WEIGHT_REF_TIME_PER_SECOND, Weight}, }; @@ -48,7 +48,7 @@ use sp_keystore::{testing::MemoryKeystore, KeystoreExt}; use sp_runtime::{ testing::{Header, H256}, traits::{BlakeTwo256, Convert, Hash, IdentityLookup}, - AccountId32, + AccountId32, TokenError, }; use std::{ops::Deref, sync::Arc}; @@ -318,6 +318,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } impl pallet_timestamp::Config for Test { @@ -1054,7 +1058,7 @@ fn transfer_allow_death_cannot_kill_account() { total_balance, ExistenceRequirement::AllowDeath, ), - pallet_balances::Error::::KeepAlive, + TokenError::Frozen, ); assert_eq!(::Currency::total_balance(&addr), total_balance); @@ -1474,25 +1478,6 @@ fn transfer_return_code() { .result .unwrap(); assert_return_code!(result, RuntimeReturnCode::TransferFailed); - - // Contract has enough total balance in order to not go below the min balance - // threshold when transfering 100 balance but this balance is reserved so - // the transfer still fails. - Balances::make_free_balance_be(&addr, min_balance + 100); - Balances::reserve(&addr, min_balance + 100).unwrap(); - let result = Contracts::bare_call( - ALICE, - addr, - 0, - GAS_LIMIT, - None, - vec![], - false, - Determinism::Deterministic, - ) - .result - .unwrap(); - assert_return_code!(result, RuntimeReturnCode::TransferFailed); }); } @@ -1569,29 +1554,6 @@ fn call_return_code() { .unwrap(); assert_return_code!(result, RuntimeReturnCode::TransferFailed); - // Contract has enough total balance in order to not go below the min balance - // threshold when transfering 100 balance but this balance is reserved so - // the transfer still fails. - Balances::make_free_balance_be(&addr_bob, min_balance + 100); - Balances::reserve(&addr_bob, min_balance + 100).unwrap(); - let result = Contracts::bare_call( - ALICE, - addr_bob.clone(), - 0, - GAS_LIMIT, - None, - AsRef::<[u8]>::as_ref(&addr_django) - .iter() - .chain(&0u32.to_le_bytes()) - .cloned() - .collect(), - false, - Determinism::Deterministic, - ) - .result - .unwrap(); - assert_return_code!(result, RuntimeReturnCode::TransferFailed); - // Contract has enough balance but callee reverts because "1" is passed. Balances::make_free_balance_be(&addr_bob, min_balance + 1000); let result = Contracts::bare_call( @@ -1683,25 +1645,6 @@ fn instantiate_return_code() { .unwrap(); assert_return_code!(result, RuntimeReturnCode::TransferFailed); - // Contract has enough total balance in order to not go below the min_balance - // threshold when transfering the balance but this balance is reserved so - // the transfer still fails. - Balances::make_free_balance_be(&addr, min_balance + 10_000); - Balances::reserve(&addr, min_balance + 10_000).unwrap(); - let result = Contracts::bare_call( - ALICE, - addr.clone(), - 0, - GAS_LIMIT, - None, - callee_hash.clone(), - false, - Determinism::Deterministic, - ) - .result - .unwrap(); - assert_return_code!(result, RuntimeReturnCode::TransferFailed); - // Contract has enough balance but the passed code hash is invalid Balances::make_free_balance_be(&addr, min_balance + 10_000); let result = Contracts::bare_call( @@ -2805,7 +2748,7 @@ fn gas_estimation_call_runtime() { // Call something trivial with a huge gas limit so that we can observe the effects // of pre-charging. This should create a difference between consumed and required. - let call = RuntimeCall::Balances(pallet_balances::Call::transfer { + let call = RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { dest: addr_callee, value: min_balance * 10, }); diff --git a/frame/conviction-voting/src/benchmarking.rs b/frame/conviction-voting/src/benchmarking.rs index 27f46507d6..8701ed7ebb 100644 --- a/frame/conviction-voting/src/benchmarking.rs +++ b/frame/conviction-voting/src/benchmarking.rs @@ -23,7 +23,11 @@ use assert_matches::assert_matches; use frame_benchmarking::v1::{account, benchmarks_instance_pallet, whitelist_account}; use frame_support::{ dispatch::RawOrigin, - traits::{fungible, Currency, Get}, + traits::{ + fungible, + tokens::{Fortitude::Polite, Preservation::Expendable}, + Currency, Get, + }, }; use sp_runtime::traits::Bounded; use sp_std::collections::btree_map::BTreeMap; @@ -257,13 +261,13 @@ benchmarks_instance_pallet! { } } - let orig_usable = >::reducible_balance(&caller, false); + let orig_usable = >::reducible_balance(&caller, Expendable, Polite); let polls = &all_polls[&class]; // Vote big on the class with the most ongoing votes of them to bump the lock and make it // hard to recompute when removed. ConvictionVoting::::vote(RawOrigin::Signed(caller.clone()).into(), polls[0], big_account_vote)?; - let now_usable = >::reducible_balance(&caller, false); + let now_usable = >::reducible_balance(&caller, Expendable, Polite); assert_eq!(orig_usable - now_usable, 100u32.into()); // Remove the vote @@ -272,7 +276,7 @@ benchmarks_instance_pallet! { // We can now unlock on `class` from 200 to 100... }: _(RawOrigin::Signed(caller.clone()), class, caller_lookup) verify { - assert_eq!(orig_usable, >::reducible_balance(&caller, false)); + assert_eq!(orig_usable, >::reducible_balance(&caller, Expendable, Polite)); } impl_benchmark_test_suite!( diff --git a/frame/conviction-voting/src/lib.rs b/frame/conviction-voting/src/lib.rs index f28953c4a3..072e570354 100644 --- a/frame/conviction-voting/src/lib.rs +++ b/frame/conviction-voting/src/lib.rs @@ -388,7 +388,10 @@ impl, I: 'static> Pallet { poll_index: PollIndexOf, vote: AccountVote>, ) -> DispatchResult { - ensure!(vote.balance() <= T::Currency::free_balance(who), Error::::InsufficientFunds); + ensure!( + vote.balance() <= T::Currency::total_balance(who), + Error::::InsufficientFunds + ); T::Polls::try_access_poll(poll_index, |poll_status| { let (tally, class) = poll_status.ensure_ongoing().ok_or(Error::::NotOngoing)?; VotingFor::::try_mutate(who, &class, |voting| { @@ -548,7 +551,7 @@ impl, I: 'static> Pallet { ) -> Result { ensure!(who != target, Error::::Nonsense); T::Polls::classes().binary_search(&class).map_err(|_| Error::::BadClass)?; - ensure!(balance <= T::Currency::free_balance(&who), Error::::InsufficientFunds); + ensure!(balance <= T::Currency::total_balance(&who), Error::::InsufficientFunds); let votes = VotingFor::::try_mutate(&who, &class, |voting| -> Result { let old = sp_std::mem::replace( diff --git a/frame/conviction-voting/src/tests.rs b/frame/conviction-voting/src/tests.rs index e01931d7ad..f33e511a16 100644 --- a/frame/conviction-voting/src/tests.rs +++ b/frame/conviction-voting/src/tests.rs @@ -51,7 +51,7 @@ frame_support::construct_runtime!( pub struct BaseFilter; impl Contains for BaseFilter { fn contains(call: &RuntimeCall) -> bool { - !matches!(call, &RuntimeCall::Balances(pallet_balances::Call::set_balance { .. })) + !matches!(call, &RuntimeCall::Balances(pallet_balances::Call::force_set_balance { .. })) } } @@ -92,6 +92,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } #[derive(Clone, PartialEq, Eq, Debug)] diff --git a/frame/democracy/src/tests.rs b/frame/democracy/src/tests.rs index 9e73576590..06fde5129c 100644 --- a/frame/democracy/src/tests.rs +++ b/frame/democracy/src/tests.rs @@ -72,7 +72,7 @@ frame_support::construct_runtime!( pub struct BaseFilter; impl Contains for BaseFilter { fn contains(call: &RuntimeCall) -> bool { - !matches!(call, &RuntimeCall::Balances(pallet_balances::Call::set_balance { .. })) + !matches!(call, &RuntimeCall::Balances(pallet_balances::Call::force_set_balance { .. })) } } @@ -144,6 +144,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } parameter_types! { pub static PreimageByteDeposit: u64 = 0; @@ -223,7 +227,7 @@ fn params_should_work() { } fn set_balance_proposal(value: u64) -> BoundedCallOf { - let inner = pallet_balances::Call::set_balance { who: 42, new_free: value, new_reserved: 0 }; + let inner = pallet_balances::Call::force_set_balance { who: 42, new_free: value }; let outer = RuntimeCall::Balances(inner); Preimage::bound(outer).unwrap() } diff --git a/frame/election-provider-multi-phase/src/benchmarking.rs b/frame/election-provider-multi-phase/src/benchmarking.rs index 4d48f17909..a5946c6a1d 100644 --- a/frame/election-provider-multi-phase/src/benchmarking.rs +++ b/frame/election-provider-multi-phase/src/benchmarking.rs @@ -219,7 +219,7 @@ frame_benchmarking::benchmarks! { finalize_signed_phase_accept_solution { let receiver = account("receiver", 0, SEED); - let initial_balance = T::Currency::minimum_balance() * 10u32.into(); + let initial_balance = T::Currency::minimum_balance() + 10u32.into(); T::Currency::make_free_balance_be(&receiver, initial_balance); let ready = Default::default(); let deposit: BalanceOf = 10u32.into(); @@ -228,7 +228,7 @@ frame_benchmarking::benchmarks! { let call_fee: BalanceOf = 30u32.into(); assert_ok!(T::Currency::reserve(&receiver, deposit)); - assert_eq!(T::Currency::free_balance(&receiver), initial_balance - 10u32.into()); + assert_eq!(T::Currency::free_balance(&receiver), T::Currency::minimum_balance()); }: { >::finalize_signed_phase_accept_solution( ready, @@ -246,17 +246,17 @@ frame_benchmarking::benchmarks! { finalize_signed_phase_reject_solution { let receiver = account("receiver", 0, SEED); - let initial_balance = T::Currency::minimum_balance().max(One::one()) * 10u32.into(); + let initial_balance = T::Currency::minimum_balance() + 10u32.into(); let deposit: BalanceOf = 10u32.into(); T::Currency::make_free_balance_be(&receiver, initial_balance); assert_ok!(T::Currency::reserve(&receiver, deposit)); - assert_eq!(T::Currency::free_balance(&receiver), initial_balance - 10u32.into()); + assert_eq!(T::Currency::free_balance(&receiver), T::Currency::minimum_balance()); assert_eq!(T::Currency::reserved_balance(&receiver), 10u32.into()); }: { >::finalize_signed_phase_reject_solution(&receiver, deposit) } verify { - assert_eq!(T::Currency::free_balance(&receiver), initial_balance - 10u32.into()); + assert_eq!(T::Currency::free_balance(&receiver), T::Currency::minimum_balance()); assert_eq!(T::Currency::reserved_balance(&receiver), 0u32.into()); } diff --git a/frame/election-provider-multi-phase/src/mock.rs b/frame/election-provider-multi-phase/src/mock.rs index da7a0cf1dd..8c18777606 100644 --- a/frame/election-provider-multi-phase/src/mock.rs +++ b/frame/election-provider-multi-phase/src/mock.rs @@ -254,6 +254,10 @@ impl pallet_balances::Config for Runtime { type MaxReserves = (); type ReserveIdentifier = [u8; 8]; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } #[derive(Default, Eq, PartialEq, Debug, Clone, Copy)] diff --git a/frame/elections-phragmen/src/lib.rs b/frame/elections-phragmen/src/lib.rs index e3eed6ec4b..d83c94db13 100644 --- a/frame/elections-phragmen/src/lib.rs +++ b/frame/elections-phragmen/src/lib.rs @@ -1248,6 +1248,10 @@ mod tests { type MaxReserves = (); type ReserveIdentifier = [u8; 8]; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } frame_support::parameter_types! { @@ -2121,7 +2125,8 @@ mod tests { assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); // User has 100 free and 50 reserved. - assert_ok!(Balances::set_balance(RuntimeOrigin::root(), 2, 100, 50)); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 2, 150)); + assert_ok!(Balances::reserve(&2, 50)); // User tries to vote with 150 tokens. assert_ok!(vote(RuntimeOrigin::signed(2), vec![4, 5], 150)); // We truncate to only their free balance, after reserving additional for voting. diff --git a/frame/examples/basic/src/tests.rs b/frame/examples/basic/src/tests.rs index 2e6a03bdd8..d9a8a4e8e1 100644 --- a/frame/examples/basic/src/tests.rs +++ b/frame/examples/basic/src/tests.rs @@ -87,6 +87,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } impl Config for Test { diff --git a/frame/executive/src/lib.rs b/frame/executive/src/lib.rs index 7a8de43140..10cf6e06b3 100644 --- a/frame/executive/src/lib.rs +++ b/frame/executive/src/lib.rs @@ -688,13 +688,10 @@ mod tests { use frame_support::{ assert_err, parameter_types, - traits::{ - ConstU32, ConstU64, ConstU8, Currency, LockIdentifier, LockableCurrency, - WithdrawReasons, - }, + traits::{fungible, ConstU32, ConstU64, ConstU8, Currency}, weights::{ConstantMultiplier, IdentityFee, RuntimeDbWeight, Weight, WeightToFee}, }; - use frame_system::{Call as SystemCall, ChainContext, LastRuntimeUpgradeInfo}; + use frame_system::{ChainContext, LastRuntimeUpgradeInfo}; use pallet_balances::Call as BalancesCall; use pallet_transaction_payment::CurrencyAdapter; @@ -899,6 +896,10 @@ mod tests { type MaxReserves = (); type ReserveIdentifier = [u8; 8]; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = ConstU32<1>; + type HoldIdentifier = (); + type MaxHolds = ConstU32<1>; } parameter_types! { @@ -972,7 +973,7 @@ mod tests { } fn call_transfer(dest: u64, value: u64) -> RuntimeCall { - RuntimeCall::Balances(BalancesCall::transfer { dest, value }) + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest, value }) } #[test] @@ -1025,13 +1026,13 @@ mod tests { block_import_works_inner( new_test_ext_v0(1), array_bytes::hex_n_into_unchecked( - "216e61b2689d1243eb56d89c9084db48e50ebebc4871d758db131432c675d7c0", + "65e953676859e7a33245908af7ad3637d6861eb90416d433d485e95e2dd174a1", ), ); block_import_works_inner( new_test_ext(1), array_bytes::hex_n_into_unchecked( - "4738b4c0aab02d6ddfa62a2a6831ccc975a9f978f7db8d7ea8e68eba8639530a", + "5a19b3d6fdb7241836349fdcbe2d9df4d4f945b949d979e31ad50bff1cbcd1c2", ), ); } @@ -1116,7 +1117,7 @@ mod tests { let mut t = new_test_ext(10000); // given: TestXt uses the encoded len as fixed Len: let xt = TestXt::new( - RuntimeCall::Balances(BalancesCall::transfer { dest: 33, value: 0 }), + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 33, value: 0 }), sign_extra(1, 0, 0), ); let encoded = xt.encode(); @@ -1139,7 +1140,10 @@ mod tests { for nonce in 0..=num_to_exhaust_block { let xt = TestXt::new( - RuntimeCall::Balances(BalancesCall::transfer { dest: 33, value: 0 }), + RuntimeCall::Balances(BalancesCall::transfer_allow_death { + dest: 33, + value: 0, + }), sign_extra(1, nonce.into(), 0), ); let res = Executive::apply_extrinsic(xt); @@ -1164,15 +1168,15 @@ mod tests { #[test] fn block_weight_and_size_is_stored_per_tx() { let xt = TestXt::new( - RuntimeCall::Balances(BalancesCall::transfer { dest: 33, value: 0 }), + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 33, value: 0 }), sign_extra(1, 0, 0), ); let x1 = TestXt::new( - RuntimeCall::Balances(BalancesCall::transfer { dest: 33, value: 0 }), + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 33, value: 0 }), sign_extra(1, 1, 0), ); let x2 = TestXt::new( - RuntimeCall::Balances(BalancesCall::transfer { dest: 33, value: 0 }), + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 33, value: 0 }), sign_extra(1, 2, 0), ); let len = xt.clone().encode().len() as u32; @@ -1258,50 +1262,30 @@ mod tests { } #[test] - fn can_pay_for_tx_fee_on_full_lock() { - let id: LockIdentifier = *b"0 "; - let execute_with_lock = |lock: WithdrawReasons| { - let mut t = new_test_ext(1); - t.execute_with(|| { - as LockableCurrency>::set_lock( - id, &1, 110, lock, - ); - let xt = TestXt::new( - RuntimeCall::System(SystemCall::remark { remark: vec![1u8] }), - sign_extra(1, 0, 0), - ); - let weight = xt.get_dispatch_info().weight + - ::BlockWeights::get() - .get(DispatchClass::Normal) - .base_extrinsic; - let fee: Balance = - ::WeightToFee::weight_to_fee( - &weight, - ); - Executive::initialize_block(&Header::new( - 1, - H256::default(), - H256::default(), - [69u8; 32].into(), - Digest::default(), - )); - - if lock == WithdrawReasons::except(WithdrawReasons::TRANSACTION_PAYMENT) { - assert!(Executive::apply_extrinsic(xt).unwrap().is_ok()); - // tx fee has been deducted. - assert_eq!(>::total_balance(&1), 111 - fee); - } else { - assert_eq!( - Executive::apply_extrinsic(xt), - Err(InvalidTransaction::Payment.into()), - ); - assert_eq!(>::total_balance(&1), 111); - } - }); - }; + fn can_not_pay_for_tx_fee_on_full_lock() { + let mut t = new_test_ext(1); + t.execute_with(|| { + as fungible::MutateFreeze>::set_freeze( + &(), + &1, + 110, + ) + .unwrap(); + let xt = TestXt::new( + RuntimeCall::System(frame_system::Call::remark { remark: vec![1u8] }), + sign_extra(1, 0, 0), + ); + Executive::initialize_block(&Header::new( + 1, + H256::default(), + H256::default(), + [69u8; 32].into(), + Digest::default(), + )); - execute_with_lock(WithdrawReasons::all()); - execute_with_lock(WithdrawReasons::except(WithdrawReasons::TRANSACTION_PAYMENT)); + assert_eq!(Executive::apply_extrinsic(xt), Err(InvalidTransaction::Payment.into()),); + assert_eq!(>::total_balance(&1), 111); + }); } #[test] @@ -1443,7 +1427,7 @@ mod tests { #[test] fn custom_runtime_upgrade_is_called_when_using_execute_block_trait() { let xt = TestXt::new( - RuntimeCall::Balances(BalancesCall::transfer { dest: 33, value: 0 }), + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 33, value: 0 }), sign_extra(1, 0, 0), ); @@ -1569,7 +1553,7 @@ mod tests { #[should_panic(expected = "Invalid inherent position for extrinsic at index 1")] fn invalid_inherent_position_fail() { let xt1 = TestXt::new( - RuntimeCall::Balances(BalancesCall::transfer { dest: 33, value: 0 }), + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 33, value: 0 }), sign_extra(1, 0, 0), ); let xt2 = TestXt::new(RuntimeCall::Custom(custom::Call::inherent_call {}), None); diff --git a/frame/fast-unstake/src/mock.rs b/frame/fast-unstake/src/mock.rs index c9a1d6d517..fbe6c4592b 100644 --- a/frame/fast-unstake/src/mock.rs +++ b/frame/fast-unstake/src/mock.rs @@ -89,6 +89,10 @@ impl pallet_balances::Config for Runtime { type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } pallet_staking_reward_curve::build! { diff --git a/frame/grandpa/src/mock.rs b/frame/grandpa/src/mock.rs index 4e4f2f79d0..a7359f6896 100644 --- a/frame/grandpa/src/mock.rs +++ b/frame/grandpa/src/mock.rs @@ -142,6 +142,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU128<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } impl pallet_timestamp::Config for Test { diff --git a/frame/identity/src/tests.rs b/frame/identity/src/tests.rs index 0e15f08ddd..ba9749172e 100644 --- a/frame/identity/src/tests.rs +++ b/frame/identity/src/tests.rs @@ -85,6 +85,10 @@ impl pallet_balances::Config for Test { type MaxReserves = (); type ReserveIdentifier = [u8; 8]; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } parameter_types! { diff --git a/frame/indices/src/mock.rs b/frame/indices/src/mock.rs index 6cf1f9d974..8bd05d04ab 100644 --- a/frame/indices/src/mock.rs +++ b/frame/indices/src/mock.rs @@ -76,6 +76,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } impl Config for Test { diff --git a/frame/lottery/src/mock.rs b/frame/lottery/src/mock.rs index 9b1a933d0a..7afd0e319d 100644 --- a/frame/lottery/src/mock.rs +++ b/frame/lottery/src/mock.rs @@ -89,6 +89,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } parameter_types! { diff --git a/frame/lottery/src/tests.rs b/frame/lottery/src/tests.rs index 09386c7e34..ae3a6c858f 100644 --- a/frame/lottery/src/tests.rs +++ b/frame/lottery/src/tests.rs @@ -23,8 +23,7 @@ use mock::{ new_test_ext, run_to_block, Balances, BalancesCall, Lottery, RuntimeCall, RuntimeOrigin, SystemCall, Test, }; -use pallet_balances::Error as BalancesError; -use sp_runtime::traits::BadOrigin; +use sp_runtime::{traits::BadOrigin, TokenError}; #[test] fn initial_state() { @@ -45,7 +44,7 @@ fn basic_end_to_end_works() { let delay = 5; let calls = vec![ RuntimeCall::Balances(BalancesCall::force_transfer { source: 0, dest: 0, value: 0 }), - RuntimeCall::Balances(BalancesCall::transfer { dest: 0, value: 0 }), + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 0, value: 0 }), ]; // Set calls for the lottery @@ -56,7 +55,10 @@ fn basic_end_to_end_works() { assert!(crate::Lottery::::get().is_some()); assert_eq!(Balances::free_balance(&1), 100); - let call = Box::new(RuntimeCall::Balances(BalancesCall::transfer { dest: 2, value: 20 })); + let call = Box::new(RuntimeCall::Balances(BalancesCall::transfer_allow_death { + dest: 2, + value: 20, + })); assert_ok!(Lottery::buy_ticket(RuntimeOrigin::signed(1), call.clone())); // 20 from the transfer, 10 from buying a ticket assert_eq!(Balances::free_balance(&1), 100 - 20 - 10); @@ -129,7 +131,7 @@ fn set_calls_works() { let calls = vec![ RuntimeCall::Balances(BalancesCall::force_transfer { source: 0, dest: 0, value: 0 }), - RuntimeCall::Balances(BalancesCall::transfer { dest: 0, value: 0 }), + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 0, value: 0 }), ]; assert_ok!(Lottery::set_calls(RuntimeOrigin::root(), calls)); @@ -137,7 +139,7 @@ fn set_calls_works() { let too_many_calls = vec![ RuntimeCall::Balances(BalancesCall::force_transfer { source: 0, dest: 0, value: 0 }), - RuntimeCall::Balances(BalancesCall::transfer { dest: 0, value: 0 }), + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 0, value: 0 }), RuntimeCall::System(SystemCall::remark { remark: vec![] }), ]; @@ -157,7 +159,7 @@ fn call_to_indices_works() { new_test_ext().execute_with(|| { let calls = vec![ RuntimeCall::Balances(BalancesCall::force_transfer { source: 0, dest: 0, value: 0 }), - RuntimeCall::Balances(BalancesCall::transfer { dest: 0, value: 0 }), + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 0, value: 0 }), ]; let indices = Lottery::calls_to_indices(&calls).unwrap().into_inner(); // Only comparing the length since it is otherwise dependant on the API @@ -166,7 +168,7 @@ fn call_to_indices_works() { let too_many_calls = vec![ RuntimeCall::Balances(BalancesCall::force_transfer { source: 0, dest: 0, value: 0 }), - RuntimeCall::Balances(BalancesCall::transfer { dest: 0, value: 0 }), + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 0, value: 0 }), RuntimeCall::System(SystemCall::remark { remark: vec![] }), ]; assert_noop!(Lottery::calls_to_indices(&too_many_calls), Error::::TooManyCalls); @@ -203,7 +205,10 @@ fn buy_ticket_works_as_simple_passthrough() { // as a simple passthrough to the real call. new_test_ext().execute_with(|| { // No lottery set up - let call = Box::new(RuntimeCall::Balances(BalancesCall::transfer { dest: 2, value: 20 })); + let call = Box::new(RuntimeCall::Balances(BalancesCall::transfer_allow_death { + dest: 2, + value: 20, + })); // This is just a basic transfer then assert_ok!(Lottery::buy_ticket(RuntimeOrigin::signed(1), call.clone())); assert_eq!(Balances::free_balance(&1), 100 - 20); @@ -212,7 +217,7 @@ fn buy_ticket_works_as_simple_passthrough() { // Lottery is set up, but too expensive to enter, so `do_buy_ticket` fails. let calls = vec![ RuntimeCall::Balances(BalancesCall::force_transfer { source: 0, dest: 0, value: 0 }), - RuntimeCall::Balances(BalancesCall::transfer { dest: 0, value: 0 }), + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 0, value: 0 }), ]; assert_ok!(Lottery::set_calls(RuntimeOrigin::root(), calls)); @@ -223,11 +228,13 @@ fn buy_ticket_works_as_simple_passthrough() { assert_eq!(TicketsCount::::get(), 0); // If call would fail, the whole thing still fails the same - let fail_call = - Box::new(RuntimeCall::Balances(BalancesCall::transfer { dest: 2, value: 1000 })); + let fail_call = Box::new(RuntimeCall::Balances(BalancesCall::transfer_allow_death { + dest: 2, + value: 1000, + })); assert_noop!( Lottery::buy_ticket(RuntimeOrigin::signed(1), fail_call), - BalancesError::::InsufficientBalance, + ArithmeticError::Underflow, ); let bad_origin_call = Box::new(RuntimeCall::Balances(BalancesCall::force_transfer { @@ -243,8 +250,10 @@ fn buy_ticket_works_as_simple_passthrough() { assert_ok!(Lottery::buy_ticket(RuntimeOrigin::signed(2), remark_call)); assert_eq!(TicketsCount::::get(), 0); - let successful_call = - Box::new(RuntimeCall::Balances(BalancesCall::transfer { dest: 2, value: 1 })); + let successful_call = Box::new(RuntimeCall::Balances(BalancesCall::transfer_allow_death { + dest: 2, + value: 1, + })); assert_ok!(Lottery::buy_ticket(RuntimeOrigin::signed(2), successful_call)); assert_eq!(TicketsCount::::get(), 1); }); @@ -256,12 +265,15 @@ fn buy_ticket_works() { // Set calls for the lottery. let calls = vec![ RuntimeCall::System(SystemCall::remark { remark: vec![] }), - RuntimeCall::Balances(BalancesCall::transfer { dest: 0, value: 0 }), + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 0, value: 0 }), ]; assert_ok!(Lottery::set_calls(RuntimeOrigin::root(), calls)); // Can't buy ticket before start - let call = Box::new(RuntimeCall::Balances(BalancesCall::transfer { dest: 2, value: 1 })); + let call = Box::new(RuntimeCall::Balances(BalancesCall::transfer_allow_death { + dest: 2, + value: 1, + })); assert_ok!(Lottery::buy_ticket(RuntimeOrigin::signed(1), call.clone())); assert_eq!(TicketsCount::::get(), 0); @@ -274,7 +286,10 @@ fn buy_ticket_works() { assert_eq!(TicketsCount::::get(), 1); // Can't buy another of the same ticket (even if call is slightly changed) - let call = Box::new(RuntimeCall::Balances(BalancesCall::transfer { dest: 3, value: 30 })); + let call = Box::new(RuntimeCall::Balances(BalancesCall::transfer_allow_death { + dest: 3, + value: 30, + })); assert_ok!(Lottery::buy_ticket(RuntimeOrigin::signed(1), call)); assert_eq!(TicketsCount::::get(), 1); @@ -302,7 +317,8 @@ fn buy_ticket_works() { #[test] fn do_buy_ticket_already_participating() { new_test_ext().execute_with(|| { - let calls = vec![RuntimeCall::Balances(BalancesCall::transfer { dest: 0, value: 0 })]; + let calls = + vec![RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 0, value: 0 })]; assert_ok!(Lottery::set_calls(RuntimeOrigin::root(), calls.clone())); assert_ok!(Lottery::start_lottery(RuntimeOrigin::root(), 1, 10, 10, false)); @@ -317,7 +333,8 @@ fn do_buy_ticket_already_participating() { #[test] fn buy_ticket_already_participating() { new_test_ext().execute_with(|| { - let calls = vec![RuntimeCall::Balances(BalancesCall::transfer { dest: 0, value: 0 })]; + let calls = + vec![RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 0, value: 0 })]; assert_ok!(Lottery::set_calls(RuntimeOrigin::root(), calls.clone())); assert_ok!(Lottery::start_lottery(RuntimeOrigin::root(), 1, 10, 10, false)); @@ -337,7 +354,8 @@ fn buy_ticket_already_participating() { #[test] fn buy_ticket_insufficient_balance() { new_test_ext().execute_with(|| { - let calls = vec![RuntimeCall::Balances(BalancesCall::transfer { dest: 0, value: 0 })]; + let calls = + vec![RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 0, value: 0 })]; assert_ok!(Lottery::set_calls(RuntimeOrigin::root(), calls.clone())); // Price set to 100. assert_ok!(Lottery::start_lottery(RuntimeOrigin::root(), 100, 10, 10, false)); @@ -352,16 +370,14 @@ fn buy_ticket_insufficient_balance() { #[test] fn do_buy_ticket_insufficient_balance() { new_test_ext().execute_with(|| { - let calls = vec![RuntimeCall::Balances(BalancesCall::transfer { dest: 0, value: 0 })]; + let calls = + vec![RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 0, value: 0 })]; assert_ok!(Lottery::set_calls(RuntimeOrigin::root(), calls.clone())); // Price set to 101. assert_ok!(Lottery::start_lottery(RuntimeOrigin::root(), 101, 10, 10, false)); // Buying fails with InsufficientBalance. - assert_noop!( - Lottery::do_buy_ticket(&1, &calls[0]), - BalancesError::::InsufficientBalance - ); + assert_noop!(Lottery::do_buy_ticket(&1, &calls[0]), TokenError::FundsUnavailable,); assert!(TicketsCount::::get().is_zero()); }); } @@ -369,13 +385,13 @@ fn do_buy_ticket_insufficient_balance() { #[test] fn do_buy_ticket_keep_alive() { new_test_ext().execute_with(|| { - let calls = vec![RuntimeCall::Balances(BalancesCall::transfer { dest: 0, value: 0 })]; + let calls = + vec![RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 0, value: 0 })]; assert_ok!(Lottery::set_calls(RuntimeOrigin::root(), calls.clone())); // Price set to 100. assert_ok!(Lottery::start_lottery(RuntimeOrigin::root(), 100, 10, 10, false)); - // Buying fails with KeepAlive. - assert_noop!(Lottery::do_buy_ticket(&1, &calls[0]), BalancesError::::KeepAlive); + assert_noop!(Lottery::do_buy_ticket(&1, &calls[0]), TokenError::NotExpendable); assert!(TicketsCount::::get().is_zero()); }); } @@ -421,7 +437,8 @@ fn choose_ticket_trivial_cases() { #[test] fn choose_account_one_participant() { new_test_ext().execute_with(|| { - let calls = vec![RuntimeCall::Balances(BalancesCall::transfer { dest: 0, value: 0 })]; + let calls = + vec![RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 0, value: 0 })]; assert_ok!(Lottery::set_calls(RuntimeOrigin::root(), calls.clone())); assert_ok!(Lottery::start_lottery(RuntimeOrigin::root(), 10, 10, 10, false)); let call = Box::new(calls[0].clone()); diff --git a/frame/multisig/src/tests.rs b/frame/multisig/src/tests.rs index c5517195d1..7e7f166802 100644 --- a/frame/multisig/src/tests.rs +++ b/frame/multisig/src/tests.rs @@ -30,6 +30,7 @@ use sp_core::H256; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, + TokenError, }; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -84,6 +85,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } pub struct TestBaseCallFilter; @@ -107,7 +112,7 @@ impl Config for Test { type WeightInfo = (); } -use pallet_balances::{Call as BalancesCall, Error as BalancesError}; +use pallet_balances::Call as BalancesCall; pub fn new_test_ext() -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); @@ -126,16 +131,16 @@ fn now() -> Timepoint { } fn call_transfer(dest: u64, value: u64) -> Box { - Box::new(RuntimeCall::Balances(BalancesCall::transfer { dest, value })) + Box::new(RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest, value })) } #[test] fn multisig_deposit_is_taken_and_returned() { new_test_ext().execute_with(|| { let multi = Multisig::multi_account_id(&[1, 2, 3][..], 2); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(3), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(1), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(2), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(3), multi, 5)); let call = call_transfer(6, 15); let call_weight = call.get_dispatch_info().weight; @@ -196,9 +201,9 @@ fn cancel_multisig_returns_deposit() { fn timepoint_checking_works() { new_test_ext().execute_with(|| { let multi = Multisig::multi_account_id(&[1, 2, 3][..], 2); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(3), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(1), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(2), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(3), multi, 5)); let call = call_transfer(6, 15); let hash = blake2_256(&call.encode()); @@ -254,9 +259,9 @@ fn timepoint_checking_works() { fn multisig_2_of_3_works() { new_test_ext().execute_with(|| { let multi = Multisig::multi_account_id(&[1, 2, 3][..], 2); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(3), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(1), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(2), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(3), multi, 5)); let call = call_transfer(6, 15); let call_weight = call.get_dispatch_info().weight; @@ -287,9 +292,9 @@ fn multisig_2_of_3_works() { fn multisig_3_of_3_works() { new_test_ext().execute_with(|| { let multi = Multisig::multi_account_id(&[1, 2, 3][..], 3); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(3), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(1), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(2), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(3), multi, 5)); let call = call_transfer(6, 15); let call_weight = call.get_dispatch_info().weight; @@ -357,9 +362,9 @@ fn cancel_multisig_works() { fn multisig_2_of_3_as_multi_works() { new_test_ext().execute_with(|| { let multi = Multisig::multi_account_id(&[1, 2, 3][..], 2); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(3), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(1), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(2), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(3), multi, 5)); let call = call_transfer(6, 15); let call_weight = call.get_dispatch_info().weight; @@ -389,9 +394,9 @@ fn multisig_2_of_3_as_multi_works() { fn multisig_2_of_3_as_multi_with_many_calls_works() { new_test_ext().execute_with(|| { let multi = Multisig::multi_account_id(&[1, 2, 3][..], 2); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(3), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(1), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(2), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(3), multi, 5)); let call1 = call_transfer(6, 10); let call1_weight = call1.get_dispatch_info().weight; @@ -440,9 +445,9 @@ fn multisig_2_of_3_as_multi_with_many_calls_works() { fn multisig_2_of_3_cannot_reissue_same_call() { new_test_ext().execute_with(|| { let multi = Multisig::multi_account_id(&[1, 2, 3][..], 2); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(3), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(1), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(2), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(3), multi, 5)); let call = call_transfer(6, 10); let call_weight = call.get_dispatch_info().weight; @@ -482,14 +487,13 @@ fn multisig_2_of_3_cannot_reissue_same_call() { call_weight )); - let err = DispatchError::from(BalancesError::::InsufficientBalance).stripped(); System::assert_last_event( pallet_multisig::Event::MultisigExecuted { approving: 3, timepoint: now(), multisig: multi, call_hash: hash, - result: Err(err), + result: Err(TokenError::FundsUnavailable.into()), } .into(), ); @@ -593,9 +597,9 @@ fn duplicate_approvals_are_ignored() { fn multisig_1_of_3_works() { new_test_ext().execute_with(|| { let multi = Multisig::multi_account_id(&[1, 2, 3][..], 1); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(3), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(1), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(2), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(3), multi, 5)); let call = call_transfer(6, 15); let hash = blake2_256(&call.encode()); @@ -646,9 +650,9 @@ fn multisig_filters() { fn weight_check_works() { new_test_ext().execute_with(|| { let multi = Multisig::multi_account_id(&[1, 2, 3][..], 2); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(3), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(1), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(2), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(3), multi, 5)); let call = call_transfer(6, 15); assert_ok!(Multisig::as_multi( @@ -682,9 +686,9 @@ fn multisig_handles_no_preimage_after_all_approve() { // the call will go through. new_test_ext().execute_with(|| { let multi = Multisig::multi_account_id(&[1, 2, 3][..], 3); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(3), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(1), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(2), multi, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(3), multi, 5)); let call = call_transfer(6, 15); let call_weight = call.get_dispatch_info().weight; diff --git a/frame/nfts/src/mock.rs b/frame/nfts/src/mock.rs index f21547b720..91ac5d6a72 100644 --- a/frame/nfts/src/mock.rs +++ b/frame/nfts/src/mock.rs @@ -89,6 +89,10 @@ impl pallet_balances::Config for Test { type MaxLocks = (); type MaxReserves = ConstU32<50>; type ReserveIdentifier = [u8; 8]; + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } parameter_types! { diff --git a/frame/nicks/src/lib.rs b/frame/nicks/src/lib.rs index b74d2e6ee4..5f510ce8e4 100644 --- a/frame/nicks/src/lib.rs +++ b/frame/nicks/src/lib.rs @@ -295,6 +295,10 @@ mod tests { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } ord_parameter_types! { diff --git a/frame/nis/README.md b/frame/nis/README.md index 4eaddae178..0c3f0c383a 100644 --- a/frame/nis/README.md +++ b/frame/nis/README.md @@ -1,2 +1,5 @@ +# NIS Module -License: Apache-2.0 +Provides a non-interactiove variant of staking. + +License: Apache-2.0 \ No newline at end of file diff --git a/frame/nis/src/benchmarking.rs b/frame/nis/src/benchmarking.rs index 6fc9fbc2bf..0cc9e7421d 100644 --- a/frame/nis/src/benchmarking.rs +++ b/frame/nis/src/benchmarking.rs @@ -21,7 +21,9 @@ use super::*; use frame_benchmarking::v1::{account, benchmarks, whitelisted_caller, BenchmarkError}; -use frame_support::traits::{nonfungible::Inspect, Currency, EnsureOrigin, Get}; +use frame_support::traits::{ + fungible::Inspect as FunInspect, nonfungible::Inspect, EnsureOrigin, Get, +}; use frame_system::RawOrigin; use sp_arithmetic::Perquintill; use sp_runtime::{ @@ -35,7 +37,7 @@ use crate::Pallet as Nis; const SEED: u32 = 0; type BalanceOf = - <::Currency as Currency<::AccountId>>::Balance; + <::Currency as FunInspect<::AccountId>>::Balance; fn fill_queues() -> Result<(), DispatchError> { // filling queues involves filling the first queue entirely and placing a single item in all @@ -45,10 +47,7 @@ fn fill_queues() -> Result<(), DispatchError> { let bids = T::MaxQueueLen::get(); let caller: T::AccountId = whitelisted_caller(); - T::Currency::make_free_balance_be( - &caller, - T::MinBid::get() * BalanceOf::::from(queues + bids), - ); + T::Currency::set_balance(&caller, T::MinBid::get() * BalanceOf::::from(queues + bids)); for _ in 0..bids { Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1)?; @@ -63,7 +62,9 @@ benchmarks! { place_bid { let l in 0..(T::MaxQueueLen::get() - 1); let caller: T::AccountId = whitelisted_caller(); - T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + let ed = T::Currency::minimum_balance(); + let bid = T::MinBid::get(); + T::Currency::set_balance(&caller, (ed + bid) * BalanceOf::::from(l + 1) + bid); for i in 0..l { Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1)?; } @@ -75,7 +76,10 @@ benchmarks! { place_bid_max { let caller: T::AccountId = whitelisted_caller(); let origin = RawOrigin::Signed(caller.clone()); - T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + let ed = T::Currency::minimum_balance(); + let bid = T::MinBid::get(); + let ql = T::MaxQueueLen::get(); + T::Currency::set_balance(&caller, (ed + bid) * BalanceOf::::from(ql + 1) + bid); for i in 0..T::MaxQueueLen::get() { Nis::::place_bid(origin.clone().into(), T::MinBid::get(), 1)?; } @@ -90,7 +94,9 @@ benchmarks! { retract_bid { let l in 1..T::MaxQueueLen::get(); let caller: T::AccountId = whitelisted_caller(); - T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + let ed = T::Currency::minimum_balance(); + let bid = T::MinBid::get(); + T::Currency::set_balance(&caller, (ed + bid) * BalanceOf::::from(l + 1) + bid); for i in 0..l { Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1)?; } @@ -104,66 +110,81 @@ benchmarks! { T::FundOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; let caller: T::AccountId = whitelisted_caller(); let bid = T::MinBid::get().max(One::one()); - T::Currency::make_free_balance_be(&caller, bid); + let ed = T::Currency::minimum_balance(); + T::Currency::set_balance(&caller, ed + bid); Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), bid, 1)?; Nis::::process_queues(Perquintill::one(), 1, 1, &mut WeightCounter::unlimited()); Nis::::communify(RawOrigin::Signed(caller.clone()).into(), 0)?; - let original = T::Currency::free_balance(&Nis::::account_id()); - T::Currency::make_free_balance_be(&Nis::::account_id(), BalanceOf::::min_value()); + let original = T::Currency::balance(&Nis::::account_id()); + T::Currency::set_balance(&Nis::::account_id(), BalanceOf::::min_value()); }: _(origin) verify { // Must fund at least 99.999% of the required amount. let missing = Perquintill::from_rational( - T::Currency::free_balance(&Nis::::account_id()), original).left_from_one(); + T::Currency::balance(&Nis::::account_id()), original).left_from_one(); assert!(missing <= Perquintill::one() / 100_000); } - thaw_private { + communify { let caller: T::AccountId = whitelisted_caller(); - T::Currency::make_free_balance_be(&caller, T::MinBid::get() * BalanceOf::::from(3u32)); - Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1)?; - Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1)?; + let bid = T::MinBid::get().max(One::one()) * 100u32.into(); + let ed = T::Currency::minimum_balance(); + T::Currency::set_balance(&caller, ed + bid + bid); + Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), bid, 1)?; + Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), bid, 1)?; Nis::::process_queues(Perquintill::one(), 1, 2, &mut WeightCounter::unlimited()); - Receipts::::mutate(0, |m_g| if let Some(ref mut g) = m_g { g.expiry = Zero::zero() }); - }: _(RawOrigin::Signed(caller.clone()), 0, None) + }: _(RawOrigin::Signed(caller.clone()), 0) verify { - assert!(Receipts::::get(0).is_none()); + assert_eq!(Nis::::owner(&0), None); } - thaw_communal { + privatize { let caller: T::AccountId = whitelisted_caller(); - T::Currency::make_free_balance_be(&caller, T::MinBid::get() * BalanceOf::::from(3u32)); - Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1)?; - Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1)?; + let bid = T::MinBid::get().max(One::one()); + let ed = T::Currency::minimum_balance(); + T::Currency::set_balance(&caller, ed + bid + bid); + Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), bid, 1)?; + Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), bid, 1)?; Nis::::process_queues(Perquintill::one(), 1, 2, &mut WeightCounter::unlimited()); - Receipts::::mutate(0, |m_g| if let Some(ref mut g) = m_g { g.expiry = Zero::zero() }); Nis::::communify(RawOrigin::Signed(caller.clone()).into(), 0)?; }: _(RawOrigin::Signed(caller.clone()), 0) verify { - assert!(Receipts::::get(0).is_none()); + assert_eq!(Nis::::owner(&0), Some(caller)); } - privatize { + thaw_private { + let whale: T::AccountId = account("whale", 0, SEED); let caller: T::AccountId = whitelisted_caller(); - T::Currency::make_free_balance_be(&caller, T::MinBid::get() * BalanceOf::::from(3u32)); - Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1)?; - Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1)?; + let bid = T::MinBid::get().max(One::one()); + let ed = T::Currency::minimum_balance(); + T::Currency::set_balance(&caller, ed + bid + bid); + // Ensure we don't get throttled. + T::Currency::set_balance(&whale, T::ThawThrottle::get().0.saturating_reciprocal_mul_ceil(T::Currency::balance(&caller))); + Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), bid, 1)?; + Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), bid, 1)?; Nis::::process_queues(Perquintill::one(), 1, 2, &mut WeightCounter::unlimited()); - Nis::::communify(RawOrigin::Signed(caller.clone()).into(), 0)?; - }: _(RawOrigin::Signed(caller.clone()), 0) + frame_system::Pallet::::set_block_number(Receipts::::get(0).unwrap().expiry); + }: _(RawOrigin::Signed(caller.clone()), 0, None) verify { - assert_eq!(Nis::::owner(&0), Some(caller)); + assert!(Receipts::::get(0).is_none()); } - communify { + thaw_communal { + let whale: T::AccountId = account("whale", 0, SEED); let caller: T::AccountId = whitelisted_caller(); - T::Currency::make_free_balance_be(&caller, T::MinBid::get() * BalanceOf::::from(3u32)); - Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1)?; - Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1)?; + let bid = T::MinBid::get().max(One::one()); + let ed = T::Currency::minimum_balance(); + T::Currency::set_balance(&caller, ed + bid + bid); + // Ensure we don't get throttled. + T::Currency::set_balance(&whale, T::ThawThrottle::get().0.saturating_reciprocal_mul_ceil(T::Currency::balance(&caller))); + Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), bid, 1)?; + Nis::::place_bid(RawOrigin::Signed(caller.clone()).into(), bid, 1)?; Nis::::process_queues(Perquintill::one(), 1, 2, &mut WeightCounter::unlimited()); + frame_system::Pallet::::set_block_number(Receipts::::get(0).unwrap().expiry); + Nis::::communify(RawOrigin::Signed(caller.clone()).into(), 0)?; }: _(RawOrigin::Signed(caller.clone()), 0) verify { - assert_eq!(Nis::::owner(&0), None); + assert!(Receipts::::get(0).is_none()); } process_queues { @@ -197,6 +218,9 @@ benchmarks! { process_bid { let who = account::("bidder", 0, SEED); + let min_bid = T::MinBid::get().max(One::one()); + let ed = T::Currency::minimum_balance(); + T::Currency::set_balance(&who, ed + min_bid); let bid = Bid { amount: T::MinBid::get(), who, @@ -216,5 +240,5 @@ benchmarks! { ) } - impl_benchmark_test_suite!(Nis, crate::mock::new_test_ext(), crate::mock::Test); + impl_benchmark_test_suite!(Nis, crate::mock::new_test_ext_empty(), crate::mock::Test); } diff --git a/frame/nis/src/lib.rs b/frame/nis/src/lib.rs index 6fd9c60a89..0b8d292ec5 100644 --- a/frame/nis/src/lib.rs +++ b/frame/nis/src/lib.rs @@ -71,21 +71,21 @@ //! //! ## Terms //! -//! - *Effective total issuance*: The total issuance of balances in the system, including all claims -//! of all outstanding receipts but excluding `IgnoredIssuance`. +//! - *Effective total issuance*: The total issuance of balances in the system, equal to the active +//! issuance plus the value of all outstanding receipts, less `IgnoredIssuance`. #![cfg_attr(not(feature = "std"), no_std)] -use frame_support::{ - dispatch::{DispatchError, DispatchResult}, - traits::fungible::{Inspect as FungibleInspect, Mutate as FungibleMutate}, +use frame_support::traits::{ + fungible::{self, Inspect as FunInspect, Mutate as FunMutate}, + tokens::{DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence}, }; pub use pallet::*; use sp_arithmetic::{traits::Unsigned, RationalArg}; use sp_core::TypedGet; use sp_runtime::{ traits::{Convert, ConvertBack}, - Perquintill, + DispatchError, Perquintill, }; mod benchmarking; @@ -117,7 +117,7 @@ where } pub struct NoCounterpart(sp_std::marker::PhantomData); -impl FungibleInspect for NoCounterpart { +impl FunInspect for NoCounterpart { type Balance = u32; fn total_issuance() -> u32 { 0 @@ -125,34 +125,30 @@ impl FungibleInspect for NoCounterpart { fn minimum_balance() -> u32 { 0 } - fn balance(_who: &T) -> u32 { + fn balance(_: &T) -> u32 { 0 } - fn reducible_balance(_who: &T, _keep_alive: bool) -> u32 { + fn total_balance(_: &T) -> u32 { 0 } - fn can_deposit( - _who: &T, - _amount: u32, - _mint: bool, - ) -> frame_support::traits::tokens::DepositConsequence { - frame_support::traits::tokens::DepositConsequence::Success + fn reducible_balance(_: &T, _: Preservation, _: Fortitude) -> u32 { + 0 } - fn can_withdraw( - _who: &T, - _amount: u32, - ) -> frame_support::traits::tokens::WithdrawConsequence { - frame_support::traits::tokens::WithdrawConsequence::Success + fn can_deposit(_: &T, _: u32, _: Provenance) -> DepositConsequence { + DepositConsequence::Success } -} -impl FungibleMutate for NoCounterpart { - fn mint_into(_who: &T, _amount: u32) -> DispatchResult { - Ok(()) + fn can_withdraw(_: &T, _: u32) -> WithdrawConsequence { + WithdrawConsequence::Success } - fn burn_from(_who: &T, _amount: u32) -> Result { - Ok(0) +} +impl fungible::Unbalanced for NoCounterpart { + fn handle_dust(_: fungible::Dust) {} + fn write_balance(_: &T, _: Self::Balance) -> Result, DispatchError> { + Ok(None) } + fn set_total_issuance(_: Self::Balance) {} } +impl FunMutate for NoCounterpart {} impl Convert for NoCounterpart { fn convert(_: Perquintill) -> u32 { 0 @@ -161,15 +157,24 @@ impl Convert for NoCounterpart { #[frame_support::pallet] pub mod pallet { - use super::{FungibleInspect, FungibleMutate}; + use super::{FunInspect, FunMutate}; pub use crate::weights::WeightInfo; use frame_support::{ pallet_prelude::*, traits::{ - nonfungible::{Inspect as NonfungibleInspect, Transfer as NonfungibleTransfer}, - Currency, Defensive, DefensiveSaturating, - ExistenceRequirement::AllowDeath, - NamedReservableCurrency, OnUnbalanced, + fungible::{ + self, + hold::{Inspect as FunHoldInspect, Mutate as FunHoldMutate}, + Balanced as FunBalanced, + }, + nonfungible::{Inspect as NftInspect, Transfer as NftTransfer}, + tokens::{ + Fortitude::Polite, + Precision::{BestEffort, Exact}, + Preservation::Expendable, + Restriction::{Free, OnHold}, + }, + Defensive, DefensiveSaturating, OnUnbalanced, }, PalletId, }; @@ -177,15 +182,14 @@ pub mod pallet { use sp_arithmetic::{PerThing, Perquintill}; use sp_runtime::{ traits::{AccountIdConversion, Bounded, Convert, ConvertBack, Saturating, Zero}, - TokenError, + Rounding, TokenError, }; use sp_std::prelude::*; type BalanceOf = - <::Currency as Currency<::AccountId>>::Balance; - type PositiveImbalanceOf = <::Currency as Currency< - ::AccountId, - >>::PositiveImbalance; + <::Currency as FunInspect<::AccountId>>::Balance; + type DebtOf = + fungible::Debt<::AccountId, ::Currency>; type ReceiptRecordOf = ReceiptRecord< ::AccountId, ::BlockNumber, @@ -209,7 +213,16 @@ pub mod pallet { type PalletId: Get; /// Currency type that this works on. - type Currency: NamedReservableCurrency; + type Currency: FunInspect + + FunMutate + + FunBalanced + + FunHoldInspect + + FunHoldMutate; + + /// The identifier of the hold reason. + + #[pallet::constant] + type HoldReason: Get<>::Reason>; /// Just the `Currency::Balance` type; we have this item to allow us to constrain it to /// `From`. @@ -231,7 +244,7 @@ pub mod pallet { type IgnoredIssuance: Get>; /// The accounting system for the fungible counterpart tokens. - type Counterpart: FungibleMutate; + type Counterpart: FunMutate; /// The system to convert an overall proportion of issuance into a number of fungible /// counterpart tokens. @@ -239,12 +252,12 @@ pub mod pallet { /// In general it's best to use `WithMaximumOf`. type CounterpartAmount: ConvertBack< Perquintill, - >::Balance, + >::Balance, >; /// Unbalanced handler to account for funds created (in case of a higher total issuance over /// freezing period). - type Deficit: OnUnbalanced>; + type Deficit: OnUnbalanced>; /// The target sum of all receipts' proportions. type Target: Get; @@ -301,12 +314,6 @@ pub mod pallet { /// The maximum proportion which may be thawed and the period over which it is reset. #[pallet::constant] type ThawThrottle: Get<(Perquintill, Self::BlockNumber)>; - - /// The name for the reserve ID. - #[pallet::constant] - type ReserveId: Get< - >::ReserveIdentifier, - >; } #[pallet::pallet] @@ -348,7 +355,7 @@ pub mod pallet { /// /// `issuance - frozen + proportion * issuance` /// - /// where `issuance = total_issuance - IgnoredIssuance` + /// where `issuance = active_issuance - IgnoredIssuance` #[derive( Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen, )] @@ -473,6 +480,9 @@ pub mod pallet { AlreadyCommunal, /// The receipt is already private. AlreadyPrivate, + Release1, + Release2, + Tah, } pub(crate) struct WeightCounter { @@ -554,16 +564,17 @@ pub mod pallet { |q| -> Result<(u32, BalanceOf), DispatchError> { let queue_full = q.len() == T::MaxQueueLen::get() as usize; ensure!(!queue_full || q[0].amount < amount, Error::::BidTooLow); - T::Currency::reserve_named(&T::ReserveId::get(), &who, amount)?; + T::Currency::hold(&T::HoldReason::get(), &who, amount)?; // queue is let mut bid = Bid { amount, who: who.clone() }; let net = if queue_full { sp_std::mem::swap(&mut q[0], &mut bid); - let _ = T::Currency::unreserve_named( - &T::ReserveId::get(), + let _ = T::Currency::release( + &T::HoldReason::get(), &bid.who, bid.amount, + BestEffort, ); Self::deposit_event(Event::::BidDropped { who: bid.who, @@ -615,19 +626,21 @@ pub mod pallet { ensure!(queue_index < queue_count, Error::::DurationTooBig); let bid = Bid { amount, who }; - let new_len = Queues::::try_mutate(duration, |q| -> Result { - let pos = q.iter().position(|i| i == &bid).ok_or(Error::::UnknownBid)?; - q.remove(pos); - Ok(q.len() as u32) - })?; + let mut queue = Queues::::get(duration); + let pos = queue.iter().position(|i| i == &bid).ok_or(Error::::UnknownBid)?; + queue.remove(pos); + let new_len = queue.len() as u32; + + T::Currency::release(&T::HoldReason::get(), &bid.who, bid.amount, BestEffort)?; + + Queues::::insert(duration, queue); QueueTotals::::mutate(|qs| { qs.bounded_resize(queue_count, (0, Zero::zero())); qs[queue_index].0 = new_len; qs[queue_index].1.saturating_reduce(bid.amount); }); - T::Currency::unreserve_named(&T::ReserveId::get(), &bid.who, bid.amount); Self::deposit_event(Event::BidRetracted { who: bid.who, amount: bid.amount, duration }); Ok(()) @@ -645,7 +658,7 @@ pub mod pallet { let issuance = Self::issuance_with(&our_account, &summary); let deficit = issuance.required.saturating_sub(issuance.holdings); ensure!(!deficit.is_zero(), Error::::AlreadyFunded); - T::Deficit::on_unbalanced(T::Currency::deposit_creating(&our_account, deficit)); + T::Deficit::on_unbalanced(T::Currency::deposit(&our_account, deficit, Exact)?); Self::deposit_event(Event::::Funded { deficit }); Ok(()) } @@ -702,6 +715,7 @@ pub mod pallet { // Multiply the proportion it is by the total issued. let our_account = Self::account_id(); let effective_issuance = Self::issuance_with(&our_account, &summary).effective; + // let amount = proportion.mul_ceil(effective_issuance); let amount = proportion * effective_issuance; receipt.proportion.saturating_reduce(proportion); @@ -710,46 +724,40 @@ pub mod pallet { let dropped = receipt.proportion.is_zero(); if amount > on_hold { - T::Currency::unreserve_named(&T::ReserveId::get(), &who, on_hold); + T::Currency::release(&T::HoldReason::get(), &who, on_hold, Exact) + .map_err(|_| Error::::Release1)?; let deficit = amount - on_hold; // Try to transfer deficit from pot to receipt owner. summary.receipts_on_hold.saturating_reduce(on_hold); on_hold = Zero::zero(); - T::Currency::transfer(&our_account, &who, deficit, AllowDeath) + T::Currency::transfer(&our_account, &who, deficit, Expendable) .map_err(|_| Error::::Unfunded)?; } else { - T::Currency::unreserve_named(&T::ReserveId::get(), &who, amount); on_hold.saturating_reduce(amount); summary.receipts_on_hold.saturating_reduce(amount); if dropped && !on_hold.is_zero() { // Reclaim any remainder: - // Transfer `excess` to the pot if we have now fully compensated for the - // receipt. - // - // This will legitimately fail if there is no pot account in existance. - // There's nothing we can do about this so we just swallow the error. - // This code is not ideal and could fail in the second phase leaving - // the system in an invalid state. It can be fixed properly with the - // new API in https://github.com/paritytech/substrate/pull/12951 - // - // Below is what it should look like then: - // let _ = T::Currency::repatriate_reserved_named( - // &T::ReserveId::get(), - // &who, - // &our_account, - // excess, - // BalanceStatus::Free, - // ).defensive(); - T::Currency::unreserve_named(&T::ReserveId::get(), &who, on_hold); - // It could theoretically be locked, so really we should be using a more - // forceful variant. But the alternative `repatriate_reserved_named` will - // fail if the destination account doesn't exist. This should be fixed when - // we move to the `fungible::*` traits, which should include a force - // transfer function to transfer the reserved balance into free balance in - // the destination regardless of locks and create it if it doesn't exist. - let _ = T::Currency::transfer(&who, &Self::account_id(), on_hold, AllowDeath); + // Transfer excess of `on_hold` to the pot if we have now fully compensated for + // the receipt. + T::Currency::transfer_on_hold( + &T::HoldReason::get(), + &who, + &our_account, + on_hold, + Exact, + Free, + Polite, + ) + .map(|_| ()) + // We ignore this error as it just means the amount we're trying to deposit is + // dust and the beneficiary account doesn't exist. + .or_else( + |e| if e == TokenError::CannotCreate.into() { Ok(()) } else { Err(e) }, + )?; summary.receipts_on_hold.saturating_reduce(on_hold); } + T::Currency::release(&T::HoldReason::get(), &who, amount, Exact) + .map_err(|_| Error::::Release2)?; } if dropped { @@ -797,7 +805,8 @@ pub mod pallet { summary.thawed.saturating_accrue(receipt.proportion); ensure!(summary.thawed <= throttle, Error::::Throttled); - T::Counterpart::burn_from(&who, T::CounterpartAmount::convert(receipt.proportion))?; + let cp_amount = T::CounterpartAmount::convert(receipt.proportion); + T::Counterpart::burn_from(&who, cp_amount, Exact, Polite)?; // Multiply the proportion it is by the total issued. let our_account = Self::account_id(); @@ -807,7 +816,7 @@ pub mod pallet { summary.proportion_owed.saturating_reduce(receipt.proportion); // Try to transfer amount owed from pot to receipt owner. - T::Currency::transfer(&our_account, &who, amount, AllowDeath) + T::Currency::transfer(&our_account, &who, amount, Expendable) .map_err(|_| Error::::Unfunded)?; Receipts::::remove(index); @@ -840,11 +849,10 @@ pub mod pallet { ensure!(owner == who, Error::::NotOwner); // Unreserve and transfer the funds to the pot. - T::Currency::unreserve_named(&T::ReserveId::get(), &who, on_hold); - // Transfer `excess` to the pot if we have now fully compensated for the receipt. - T::Currency::transfer(&who, &Self::account_id(), on_hold, AllowDeath) + let reason = T::HoldReason::get(); + let us = Self::account_id(); + T::Currency::transfer_on_hold(&reason, &who, &us, on_hold, Exact, Free, Polite) .map_err(|_| Error::::Unfunded)?; - // TODO #12951: ^^^ The above should be done in a single operation `transfer_on_hold`. // Record that we've moved the amount reserved. let mut summary: SummaryRecordOf = Summary::::get(); @@ -881,16 +889,20 @@ pub mod pallet { let effective_issuance = Self::issuance_with(&our_account, &summary).effective; let max_amount = receipt.proportion * effective_issuance; // Avoid trying to place more in the account's reserve than we have available in the pot - let amount = max_amount.min(T::Currency::free_balance(&our_account)); + let amount = max_amount.min(T::Currency::balance(&our_account)); // Burn fungible counterparts. - T::Counterpart::burn_from(&who, T::CounterpartAmount::convert(receipt.proportion))?; + T::Counterpart::burn_from( + &who, + T::CounterpartAmount::convert(receipt.proportion), + Exact, + Polite, + )?; // Transfer the funds from the pot to the owner and reserve - T::Currency::transfer(&Self::account_id(), &who, amount, AllowDeath) - .map_err(|_| Error::::Unfunded)?; - T::Currency::reserve_named(&T::ReserveId::get(), &who, amount)?; - // TODO: ^^^ The above should be done in a single operation `transfer_and_hold`. + let reason = T::HoldReason::get(); + let us = Self::account_id(); + T::Currency::transfer_and_hold(&reason, &us, &who, amount, Exact, Expendable, Polite)?; // Record that we've moved the amount reserved. summary.receipts_on_hold.saturating_accrue(amount); @@ -920,7 +932,7 @@ pub mod pallet { pub required: Balance, } - impl NonfungibleInspect for Pallet { + impl NftInspect for Pallet { type ItemId = ReceiptIndex; fn owner(item: &ReceiptIndex) -> Option { @@ -939,31 +951,19 @@ pub mod pallet { } } - impl NonfungibleTransfer for Pallet { - fn transfer(index: &ReceiptIndex, destination: &T::AccountId) -> DispatchResult { + impl NftTransfer for Pallet { + fn transfer(index: &ReceiptIndex, dest: &T::AccountId) -> DispatchResult { let mut item = Receipts::::get(index).ok_or(TokenError::UnknownAsset)?; let (owner, on_hold) = item.owner.take().ok_or(Error::::AlreadyCommunal)?; - // TODO: This should all be replaced by a single call `transfer_held`. - let shortfall = T::Currency::unreserve_named(&T::ReserveId::get(), &owner, on_hold); - if !shortfall.is_zero() { - let _ = - T::Currency::reserve_named(&T::ReserveId::get(), &owner, on_hold - shortfall); - return Err(TokenError::NoFunds.into()) - } - if let Err(e) = T::Currency::transfer(&owner, destination, on_hold, AllowDeath) { - let _ = T::Currency::reserve_named(&T::ReserveId::get(), &owner, on_hold); - return Err(e) - } - // This can never fail, and if it somehow does, then we can't handle this gracefully. - let _ = - T::Currency::reserve_named(&T::ReserveId::get(), destination, on_hold).defensive(); + let reason = T::HoldReason::get(); + T::Currency::transfer_on_hold(&reason, &owner, dest, on_hold, Exact, OnHold, Polite)?; - item.owner = Some((destination.clone(), on_hold)); + item.owner = Some((dest.clone(), on_hold)); Receipts::::insert(&index, &item); Pallet::::deposit_event(Event::::Transferred { from: owner, - to: destination.clone(), + to: dest.clone(), index: *index, }); Ok(()) @@ -997,9 +997,9 @@ pub mod pallet { summary: &SummaryRecordOf, ) -> IssuanceInfo> { let total_issuance = - T::Currency::total_issuance().saturating_sub(T::IgnoredIssuance::get()); + T::Currency::active_issuance().saturating_sub(T::IgnoredIssuance::get()); let holdings = - T::Currency::free_balance(our_account).saturating_add(summary.receipts_on_hold); + T::Currency::balance(our_account).saturating_add(summary.receipts_on_hold); let other = total_issuance.saturating_sub(holdings); let effective = summary.proportion_owed.left_from_one().saturating_reciprocal_mul(other); @@ -1136,7 +1136,9 @@ pub mod pallet { // Now to activate the bid... let n = amount; let d = issuance.effective; - let proportion = Perquintill::from_rational(n, d); + let proportion = + Perquintill::from_rational_with_rounding(n, d, Rounding::NearestPrefDown) + .defensive_unwrap_or_default(); let who = bid.who; let index = summary.index; summary.proportion_owed.defensive_saturating_accrue(proportion); diff --git a/frame/nis/src/mock.rs b/frame/nis/src/mock.rs index 8594c88a59..0ca6690936 100644 --- a/frame/nis/src/mock.rs +++ b/frame/nis/src/mock.rs @@ -19,13 +19,18 @@ use crate::{self as pallet_nis, Perquintill, WithMaximumOf}; +use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{ ord_parameter_types, parameter_types, - traits::{ConstU16, ConstU32, ConstU64, Currency, OnFinalize, OnInitialize, StorageMapShim}, + traits::{ + fungible::Inspect, ConstU16, ConstU32, ConstU64, Everything, OnFinalize, OnInitialize, + StorageMapShim, + }, weights::Weight, PalletId, }; use pallet_balances::{Instance1, Instance2}; +use scale_info::TypeInfo; use sp_core::{ConstU128, H256}; use sp_runtime::{ testing::Header, @@ -35,6 +40,8 @@ use sp_runtime::{ type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; +pub type Balance = u64; + // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( pub enum Test where @@ -50,7 +57,7 @@ frame_support::construct_runtime!( ); impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; + type BaseCallFilter = Everything; type BlockWeights = (); type BlockLength = (); type RuntimeOrigin = RuntimeOrigin; @@ -67,35 +74,45 @@ impl frame_system::Config for Test { type DbWeight = (); type Version = (); type PalletInfo = PalletInfo; - type AccountData = pallet_balances::AccountData; + type AccountData = pallet_balances::AccountData; type OnNewAccount = (); type OnKilledAccount = (); type SystemWeightInfo = (); type SS58Prefix = ConstU16<42>; type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; + type MaxConsumers = ConstU32<16>; } impl pallet_balances::Config for Test { - type Balance = u64; + type Balance = Balance; type DustRemoval = (); type RuntimeEvent = RuntimeEvent; - type ExistentialDeposit = frame_support::traits::ConstU64<1>; + type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); type MaxLocks = (); type MaxReserves = ConstU32<1>; type ReserveIdentifier = [u8; 8]; + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = HoldIdentifier; + type MaxHolds = ConstU32<1>; +} + +#[derive( + Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, Debug, TypeInfo, +)] +pub enum HoldIdentifier { + Nis, } impl pallet_balances::Config for Test { type Balance = u128; type DustRemoval = (); type RuntimeEvent = RuntimeEvent; - type ExistentialDeposit = frame_support::traits::ConstU128<1>; + type ExistentialDeposit = ConstU128<1>; type AccountStore = StorageMapShim< pallet_balances::Account, - frame_system::Provider, u64, pallet_balances::AccountData, >; @@ -103,16 +120,20 @@ impl pallet_balances::Config for Test { type MaxLocks = (); type MaxReserves = (); type ReserveIdentifier = [u8; 8]; + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } parameter_types! { - pub IgnoredIssuance: u64 = Balances::total_balance(&0); // Account zero is ignored. + pub IgnoredIssuance: Balance = Balances::total_balance(&0); // Account zero is ignored. pub const NisPalletId: PalletId = PalletId(*b"py/nis "); pub static Target: Perquintill = Perquintill::zero(); pub const MinReceipt: Perquintill = Perquintill::from_percent(1); pub const ThawThrottle: (Perquintill, u64) = (Perquintill::from_percent(25), 5); pub static MaxIntakeWeight: Weight = Weight::from_parts(2_000_000_000_000, 0); - pub const ReserveId: [u8; 8] = *b"py/nis "; + pub const HoldReason: HoldIdentifier = HoldIdentifier::Nis; } ord_parameter_types! { @@ -140,7 +161,7 @@ impl pallet_nis::Config for Test { type MaxIntakeWeight = MaxIntakeWeight; type MinReceipt = MinReceipt; type ThawThrottle = ThawThrottle; - type ReserveId = ReserveId; + type HoldReason = HoldReason; } // This function basically just builds a genesis storage key/value store according to @@ -155,6 +176,13 @@ pub fn new_test_ext() -> sp_io::TestExternalities { t.into() } +// This function basically just builds a genesis storage key/value store according to +// our desired mockup, but without any balances. +#[cfg(feature = "runtime-benchmarks")] +pub fn new_test_ext_empty() -> sp_io::TestExternalities { + frame_system::GenesisConfig::default().build_storage::().unwrap().into() +} + pub fn run_to_block(n: u64) { while System::block_number() < n { Nis::on_finalize(System::block_number()); diff --git a/frame/nis/src/tests.rs b/frame/nis/src/tests.rs index d0808367ee..7350da97dc 100644 --- a/frame/nis/src/tests.rs +++ b/frame/nis/src/tests.rs @@ -22,19 +22,22 @@ use crate::{mock::*, Error}; use frame_support::{ assert_noop, assert_ok, traits::{ + fungible::{hold::Inspect as InspectHold, Inspect as FunInspect, Mutate as FunMutate}, nonfungible::{Inspect, Transfer}, - Currency, + tokens::{Fortitude::Force, Precision::Exact}, }, }; -use pallet_balances::{Error as BalancesError, Instance1}; use sp_arithmetic::Perquintill; -use sp_runtime::{Saturating, TokenError}; +use sp_runtime::{ + Saturating, + TokenError::{self, FundsUnavailable}, +}; -fn pot() -> u64 { +fn pot() -> Balance { Balances::free_balance(&Nis::account_id()) } -fn holdings() -> u64 { +fn holdings() -> Balance { Nis::issuance().holdings } @@ -42,8 +45,8 @@ fn signed(who: u64) -> RuntimeOrigin { RuntimeOrigin::signed(who) } -fn enlarge(amount: u64, max_bids: u32) { - let summary: SummaryRecord = Summary::::get(); +fn enlarge(amount: Balance, max_bids: u32) { + let summary: SummaryRecord = Summary::::get(); let increase_in_proportion_owed = Perquintill::from_rational(amount, Nis::issuance().effective); let target = summary.proportion_owed.saturating_add(increase_in_proportion_owed); Nis::process_queues(target, u32::max_value(), max_bids, &mut WeightCounter::unlimited()); @@ -75,10 +78,7 @@ fn place_bid_works() { new_test_ext().execute_with(|| { run_to_block(1); assert_noop!(Nis::place_bid(signed(1), 1, 2), Error::::AmountTooSmall); - assert_noop!( - Nis::place_bid(signed(1), 101, 2), - BalancesError::::InsufficientBalance - ); + assert_noop!(Nis::place_bid(signed(1), 101, 2), FundsUnavailable); assert_noop!(Nis::place_bid(signed(1), 10, 4), Error::::DurationTooBig); assert_ok!(Nis::place_bid(signed(1), 10, 2)); assert_eq!(Balances::reserved_balance(1), 10); @@ -451,16 +451,16 @@ fn communify_works() { assert_noop!(Nis::thaw_communal(signed(1), 1), Error::::UnknownReceipt); // Transfer some of the fungibles away. - assert_ok!(NisBalances::transfer(signed(1), 2, 100_000)); + assert_ok!(NisBalances::transfer_allow_death(signed(1), 2, 100_000)); assert_eq!(NisBalances::free_balance(&1), 2_000_000); assert_eq!(NisBalances::free_balance(&2), 100_000); // Communal thawing with the correct index is not possible now. - assert_noop!(Nis::thaw_communal(signed(1), 0), TokenError::NoFunds); - assert_noop!(Nis::thaw_communal(signed(2), 0), TokenError::NoFunds); + assert_noop!(Nis::thaw_communal(signed(1), 0), TokenError::FundsUnavailable); + assert_noop!(Nis::thaw_communal(signed(2), 0), TokenError::FundsUnavailable); // Transfer the rest to 2... - assert_ok!(NisBalances::transfer(signed(1), 2, 2_000_000)); + assert_ok!(NisBalances::transfer_allow_death(signed(1), 2, 2_000_000)); assert_eq!(NisBalances::free_balance(&1), 0); assert_eq!(NisBalances::free_balance(&2), 2_100_000); @@ -487,8 +487,8 @@ fn privatize_works() { assert_ok!(Nis::communify(signed(1), 0)); // Transfer the fungibles to #2 - assert_ok!(NisBalances::transfer(signed(1), 2, 2_100_000)); - assert_noop!(Nis::privatize(signed(1), 0), TokenError::NoFunds); + assert_ok!(NisBalances::transfer_allow_death(signed(1), 2, 2_100_000)); + assert_noop!(Nis::privatize(signed(1), 0), TokenError::FundsUnavailable); // Privatize assert_ok!(Nis::privatize(signed(2), 0)); @@ -513,16 +513,16 @@ fn privatize_and_thaw_with_another_receipt_works() { assert_ok!(Nis::communify(signed(2), 1)); // Transfer half of fungibles to #3 from each of #1 and #2, and the other half from #2 to #4 - assert_ok!(NisBalances::transfer(signed(1), 3, 1_050_000)); - assert_ok!(NisBalances::transfer(signed(2), 3, 1_050_000)); - assert_ok!(NisBalances::transfer(signed(2), 4, 1_050_000)); + assert_ok!(NisBalances::transfer_allow_death(signed(1), 3, 1_050_000)); + assert_ok!(NisBalances::transfer_allow_death(signed(2), 3, 1_050_000)); + assert_ok!(NisBalances::transfer_allow_death(signed(2), 4, 1_050_000)); // #3 privatizes, partially thaws, then re-communifies with #0, then transfers the fungibles // to #2 assert_ok!(Nis::privatize(signed(3), 0)); assert_ok!(Nis::thaw_private(signed(3), 0, Some(Perquintill::from_percent(5)))); assert_ok!(Nis::communify(signed(3), 0)); - assert_ok!(NisBalances::transfer(signed(3), 1, 1_050_000)); + assert_ok!(NisBalances::transfer_allow_death(signed(3), 1, 1_050_000)); // #1 now has enough to thaw using receipt 1 assert_ok!(Nis::thaw_communal(signed(1), 1)); @@ -536,17 +536,21 @@ fn privatize_and_thaw_with_another_receipt_works() { fn communal_thaw_when_issuance_higher_works() { new_test_ext().execute_with(|| { run_to_block(1); + assert_ok!(Balances::transfer_allow_death(signed(2), 1, 1)); assert_ok!(Nis::place_bid(signed(1), 100, 1)); enlarge(100, 1); + assert_eq!(Balances::total_balance(&1), 101); assert_ok!(Nis::communify(signed(1), 0)); + assert_eq!(Balances::total_balance_on_hold(&1), 0); + assert_eq!(Balances::total_balance(&1), 1); - assert_eq!(NisBalances::free_balance(1), 5_250_000); // (25% of 21m) + assert_eq!(NisBalances::free_balance(1), 5_250_000); // (12.5% of 21m) // Everybody else's balances goes up by 50% - Balances::make_free_balance_be(&2, 150); - Balances::make_free_balance_be(&3, 150); - Balances::make_free_balance_be(&4, 150); + assert_ok!(Balances::mint_into(&2, 50)); + assert_ok!(Balances::mint_into(&3, 50)); + assert_ok!(Balances::mint_into(&4, 50)); run_to_block(4); @@ -556,16 +560,20 @@ fn communal_thaw_when_issuance_higher_works() { assert_ok!(Nis::fund_deficit(signed(1))); // Transfer counterparts away... - assert_ok!(NisBalances::transfer(signed(1), 2, 250_000)); + assert_ok!(NisBalances::transfer_allow_death(signed(1), 2, 125_000)); // ...and it's not thawable. - assert_noop!(Nis::thaw_communal(signed(1), 0), TokenError::NoFunds); + assert_noop!(Nis::thaw_communal(signed(1), 0), TokenError::FundsUnavailable); // Transfer counterparts back... - assert_ok!(NisBalances::transfer(signed(2), 1, 250_000)); + assert_ok!(NisBalances::transfer_allow_death(signed(2), 1, 125_000)); // ...and it is. assert_ok!(Nis::thaw_communal(signed(1), 0)); + assert_eq!(Balances::total_balance(&1), 151); + assert_ok!(Balances::transfer_allow_death(signed(1), 2, 1)); + assert_eq!(Balances::total_balance(&1), 150); assert_eq!(Balances::free_balance(1), 150); + assert_eq!(Balances::total_balance_on_hold(&1), 0); assert_eq!(Balances::reserved_balance(1), 0); }); } @@ -574,13 +582,14 @@ fn communal_thaw_when_issuance_higher_works() { fn private_thaw_when_issuance_higher_works() { new_test_ext().execute_with(|| { run_to_block(1); + assert_ok!(Balances::transfer_allow_death(signed(2), 1, 1)); assert_ok!(Nis::place_bid(signed(1), 100, 1)); enlarge(100, 1); // Everybody else's balances goes up by 50% - Balances::make_free_balance_be(&2, 150); - Balances::make_free_balance_be(&3, 150); - Balances::make_free_balance_be(&4, 150); + assert_ok!(Balances::mint_into(&2, 50)); + assert_ok!(Balances::mint_into(&3, 50)); + assert_ok!(Balances::mint_into(&4, 50)); run_to_block(4); @@ -591,6 +600,7 @@ fn private_thaw_when_issuance_higher_works() { assert_ok!(Nis::thaw_private(signed(1), 0, None)); + assert_ok!(Balances::transfer_allow_death(signed(1), 2, 1)); assert_eq!(Balances::free_balance(1), 150); assert_eq!(Balances::reserved_balance(1), 0); }); @@ -601,15 +611,16 @@ fn thaw_with_ignored_issuance_works() { new_test_ext().execute_with(|| { run_to_block(1); // Give account zero some balance. - Balances::make_free_balance_be(&0, 200); + assert_ok!(Balances::mint_into(&0, 200)); + assert_ok!(Balances::transfer_allow_death(signed(2), 1, 1)); assert_ok!(Nis::place_bid(signed(1), 100, 1)); enlarge(100, 1); // Account zero transfers 50 into everyone else's accounts. - assert_ok!(Balances::transfer(signed(0), 2, 50)); - assert_ok!(Balances::transfer(signed(0), 3, 50)); - assert_ok!(Balances::transfer(signed(0), 4, 50)); + assert_ok!(Balances::transfer_allow_death(signed(0), 2, 50)); + assert_ok!(Balances::transfer_allow_death(signed(0), 3, 50)); + assert_ok!(Balances::transfer_allow_death(signed(0), 4, 50)); run_to_block(4); // Unfunded initially... @@ -620,6 +631,7 @@ fn thaw_with_ignored_issuance_works() { assert_ok!(Nis::thaw_private(signed(1), 0, None)); // Account zero changes have been ignored. + assert_ok!(Balances::transfer_allow_death(signed(1), 2, 1)); assert_eq!(Balances::free_balance(1), 150); assert_eq!(Balances::reserved_balance(1), 0); }); @@ -629,17 +641,19 @@ fn thaw_with_ignored_issuance_works() { fn thaw_when_issuance_lower_works() { new_test_ext().execute_with(|| { run_to_block(1); + assert_ok!(Balances::transfer_allow_death(signed(2), 1, 1)); assert_ok!(Nis::place_bid(signed(1), 100, 1)); enlarge(100, 1); // Everybody else's balances goes down by 25% - Balances::make_free_balance_be(&2, 75); - Balances::make_free_balance_be(&3, 75); - Balances::make_free_balance_be(&4, 75); + assert_ok!(Balances::burn_from(&2, 25, Exact, Force)); + assert_ok!(Balances::burn_from(&3, 25, Exact, Force)); + assert_ok!(Balances::burn_from(&4, 25, Exact, Force)); run_to_block(4); assert_ok!(Nis::thaw_private(signed(1), 0, None)); + assert_ok!(Balances::transfer_allow_death(signed(1), 2, 1)); assert_eq!(Balances::free_balance(1), 75); assert_eq!(Balances::reserved_balance(1), 0); }); @@ -649,15 +663,16 @@ fn thaw_when_issuance_lower_works() { fn multiple_thaws_works() { new_test_ext().execute_with(|| { run_to_block(1); + assert_ok!(Balances::transfer_allow_death(signed(3), 1, 1)); assert_ok!(Nis::place_bid(signed(1), 40, 1)); assert_ok!(Nis::place_bid(signed(1), 60, 1)); assert_ok!(Nis::place_bid(signed(2), 50, 1)); enlarge(200, 3); // Double everyone's free balances. - Balances::make_free_balance_be(&2, 100); - Balances::make_free_balance_be(&3, 200); - Balances::make_free_balance_be(&4, 200); + assert_ok!(Balances::mint_into(&2, 50)); + assert_ok!(Balances::mint_into(&3, 100)); + assert_ok!(Balances::mint_into(&4, 100)); assert_ok!(Nis::fund_deficit(signed(1))); run_to_block(4); @@ -667,8 +682,11 @@ fn multiple_thaws_works() { run_to_block(5); assert_ok!(Nis::thaw_private(signed(2), 2, None)); + assert_ok!(Balances::transfer_allow_death(signed(1), 3, 1)); assert_eq!(Balances::free_balance(1), 200); assert_eq!(Balances::free_balance(2), 200); + assert_eq!(Balances::total_balance(&1), 200); + assert_eq!(Balances::total_balance(&2), 200); }); } @@ -676,15 +694,16 @@ fn multiple_thaws_works() { fn multiple_thaws_works_in_alternative_thaw_order() { new_test_ext().execute_with(|| { run_to_block(1); + assert_ok!(Balances::transfer_allow_death(signed(3), 1, 1)); assert_ok!(Nis::place_bid(signed(1), 40, 1)); assert_ok!(Nis::place_bid(signed(1), 60, 1)); assert_ok!(Nis::place_bid(signed(2), 50, 1)); enlarge(200, 3); // Double everyone's free balances. - Balances::make_free_balance_be(&2, 100); - Balances::make_free_balance_be(&3, 200); - Balances::make_free_balance_be(&4, 200); + assert_ok!(Balances::mint_into(&2, 50)); + assert_ok!(Balances::mint_into(&3, 100)); + assert_ok!(Balances::mint_into(&4, 100)); assert_ok!(Nis::fund_deficit(signed(1))); run_to_block(4); @@ -695,8 +714,11 @@ fn multiple_thaws_works_in_alternative_thaw_order() { run_to_block(5); assert_ok!(Nis::thaw_private(signed(1), 1, None)); + assert_ok!(Balances::transfer_allow_death(signed(1), 3, 1)); assert_eq!(Balances::free_balance(1), 200); assert_eq!(Balances::free_balance(2), 200); + assert_eq!(Balances::total_balance(&1), 200); + assert_eq!(Balances::total_balance(&2), 200); }); } diff --git a/frame/nomination-pools/benchmarking/src/mock.rs b/frame/nomination-pools/benchmarking/src/mock.rs index 4a1a52868e..cffb712ea2 100644 --- a/frame/nomination-pools/benchmarking/src/mock.rs +++ b/frame/nomination-pools/benchmarking/src/mock.rs @@ -75,6 +75,10 @@ impl pallet_balances::Config for Runtime { type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } pallet_staking_reward_curve::build! { diff --git a/frame/nomination-pools/src/mock.rs b/frame/nomination-pools/src/mock.rs index c6b094f0aa..6d83ef61de 100644 --- a/frame/nomination-pools/src/mock.rs +++ b/frame/nomination-pools/src/mock.rs @@ -196,6 +196,10 @@ impl pallet_balances::Config for Runtime { type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } pub struct BalanceToU256; diff --git a/frame/nomination-pools/src/tests.rs b/frame/nomination-pools/src/tests.rs index a4fda2e5d9..4cb255e23b 100644 --- a/frame/nomination-pools/src/tests.rs +++ b/frame/nomination-pools/src/tests.rs @@ -39,6 +39,16 @@ macro_rules! member_unbonding_eras { pub const DEFAULT_ROLES: PoolRoles = PoolRoles { depositor: 10, root: Some(900), nominator: Some(901), bouncer: Some(902) }; +fn deposit_rewards(r: u128) { + let b = Balances::free_balance(&default_reward_account()).checked_add(r).unwrap(); + Balances::make_free_balance_be(&default_reward_account(), b); +} + +fn remove_rewards(r: u128) { + let b = Balances::free_balance(&default_reward_account()).checked_sub(r).unwrap(); + Balances::make_free_balance_be(&default_reward_account(), b); +} + #[test] fn test_setup_works() { ExtBuilder::default().build_and_execute(|| { @@ -469,6 +479,8 @@ mod sub_pools { } mod join { + use sp_runtime::TokenError; + use super::*; #[test] @@ -592,7 +604,7 @@ mod join { // Balance needs to be gt Balance::MAX / `MaxPointsToBalance` assert_noop!( Pools::join(RuntimeOrigin::signed(11), 5, 123), - pallet_balances::Error::::InsufficientBalance, + TokenError::FundsUnavailable, ); StakingMock::set_bonded_balance(Pools::create_bonded_account(1), max_points_to_balance); @@ -749,7 +761,7 @@ mod claim_payout { // and the reward pool has earned 100 in rewards assert_eq!(Balances::free_balance(default_reward_account()), ed); - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 100)); + deposit_rewards(100); let _ = pool_events_since_last_call(); @@ -796,7 +808,7 @@ mod claim_payout { assert_eq!(Balances::free_balance(&default_reward_account()), ed); // Given the reward pool has some new rewards - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 50)); + deposit_rewards(50); // When assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -825,7 +837,7 @@ mod claim_payout { assert_eq!(Balances::free_balance(&default_reward_account()), ed + 25); // Given del 50 hasn't claimed and the reward pools has just earned 50 - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 50)); + deposit_rewards(50); assert_eq!(Balances::free_balance(&default_reward_account()), ed + 75); // When @@ -855,7 +867,7 @@ mod claim_payout { assert_eq!(Balances::free_balance(&default_reward_account()), ed + 20); // Given del 40 hasn't claimed and the reward pool has just earned 400 - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 400)); + deposit_rewards(400); assert_eq!(Balances::free_balance(&default_reward_account()), ed + 420); // When @@ -874,7 +886,7 @@ mod claim_payout { assert_eq!(Balances::free_balance(&default_reward_account()), ed + 380); // Given del 40 + del 50 haven't claimed and the reward pool has earned 20 - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 20)); + deposit_rewards(20); assert_eq!(Balances::free_balance(&default_reward_account()), ed + 400); // When @@ -974,7 +986,7 @@ mod claim_payout { ); // The pool earns 10 points - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 10)); + deposit_rewards(10); assert_ok!(Pools::do_reward_payout( &10, @@ -1010,7 +1022,7 @@ mod claim_payout { assert_eq!(reward_pool, rew(0, 0, 0)); // Given the pool has earned some rewards for the first time - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 5)); + deposit_rewards(5); // When let payout = @@ -1031,7 +1043,7 @@ mod claim_payout { assert_eq!(member, del(0.5)); // Given the pool has earned rewards again - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 10)); + deposit_rewards(10); // When let payout = @@ -1090,7 +1102,7 @@ mod claim_payout { assert_eq!(bonded_pool.points, 100); // and the reward pool has earned 100 in rewards - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 100)); + deposit_rewards(100); // When let payout = @@ -1135,7 +1147,7 @@ mod claim_payout { assert_eq!(reward_pool, rew(0, 0, 100)); // Given the reward pool has some new rewards - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 50)); + deposit_rewards(50); // When let payout = @@ -1166,7 +1178,7 @@ mod claim_payout { assert_eq!(reward_pool, rew(0, 0, 125)); // Given del_50 hasn't claimed and the reward pools has just earned 50 - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 50)); + deposit_rewards(50); // When let payout = @@ -1197,7 +1209,7 @@ mod claim_payout { assert_eq!(reward_pool, rew(0, 0, 180)); // Given del_40 hasn't claimed and the reward pool has just earned 400 - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 400)); + deposit_rewards(400); // When let payout = @@ -1214,7 +1226,7 @@ mod claim_payout { assert_eq!(reward_pool, rew(0, 0, 220)); // Given del_40 + del_50 haven't claimed and the reward pool has earned 20 - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 20)); + deposit_rewards(20); // When let payout = @@ -1252,14 +1264,14 @@ mod claim_payout { fn rewards_distribution_is_fair_basic() { ExtBuilder::default().build_and_execute(|| { // reward pool by 10. - Balances::mutate_account(&default_reward_account(), |f| f.free += 10).unwrap(); + deposit_rewards(10); // 20 joins afterwards. Balances::make_free_balance_be(&20, Balances::minimum_balance() + 10); assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10, 1)); // reward by another 20 - Balances::mutate_account(&default_reward_account(), |f| f.free += 20).unwrap(); + deposit_rewards(20); // 10 should claim 10 + 10, 20 should claim 20 / 2. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -1276,7 +1288,7 @@ mod claim_payout { ); // any upcoming rewards are shared equally. - Balances::mutate_account(&default_reward_account(), |f| f.free += 20).unwrap(); + deposit_rewards(20); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); @@ -1296,12 +1308,12 @@ mod claim_payout { // basically checks the case where the amount of rewards is less than the pool shares. for // this, we have to rely on fixed point arithmetic. ExtBuilder::default().build_and_execute(|| { - Balances::mutate_account(&default_reward_account(), |f| f.free += 3).unwrap(); + deposit_rewards(3); Balances::make_free_balance_be(&20, Balances::minimum_balance() + 10); assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10, 1)); - Balances::mutate_account(&default_reward_account(), |f| f.free += 6).unwrap(); + deposit_rewards(6); // 10 should claim 3, 20 should claim 3 + 3. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -1319,7 +1331,7 @@ mod claim_payout { ); // any upcoming rewards are shared equally. - Balances::mutate_account(&default_reward_account(), |f| f.free += 8).unwrap(); + deposit_rewards(8); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); @@ -1333,7 +1345,7 @@ mod claim_payout { ); // uneven upcoming rewards are shared equally, rounded down. - Balances::mutate_account(&default_reward_account(), |f| f.free += 7).unwrap(); + deposit_rewards(7); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); @@ -1353,17 +1365,17 @@ mod claim_payout { ExtBuilder::default().build_and_execute(|| { let ed = Balances::minimum_balance(); - Balances::mutate_account(&default_reward_account(), |f| f.free += 30).unwrap(); + deposit_rewards(30); Balances::make_free_balance_be(&20, ed + 10); assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10, 1)); - Balances::mutate_account(&default_reward_account(), |f| f.free += 100).unwrap(); + deposit_rewards(100); Balances::make_free_balance_be(&30, ed + 10); assert_ok!(Pools::join(RuntimeOrigin::signed(30), 10, 1)); - Balances::mutate_account(&default_reward_account(), |f| f.free += 60).unwrap(); + deposit_rewards(60); // 10 should claim 10, 20 should claim nothing. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -1384,7 +1396,7 @@ mod claim_payout { ); // any upcoming rewards are shared equally. - Balances::mutate_account(&default_reward_account(), |f| f.free += 30).unwrap(); + deposit_rewards(30); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); @@ -1407,7 +1419,7 @@ mod claim_payout { let ed = Balances::minimum_balance(); assert_eq!(Pools::api_pending_rewards(10), Some(0)); - Balances::mutate_account(&default_reward_account(), |f| f.free += 30).unwrap(); + deposit_rewards(30); assert_eq!(Pools::api_pending_rewards(10), Some(30)); assert_eq!(Pools::api_pending_rewards(20), None); @@ -1417,7 +1429,7 @@ mod claim_payout { assert_eq!(Pools::api_pending_rewards(10), Some(30)); assert_eq!(Pools::api_pending_rewards(20), Some(0)); - Balances::mutate_account(&default_reward_account(), |f| f.free += 100).unwrap(); + deposit_rewards(100); assert_eq!(Pools::api_pending_rewards(10), Some(30 + 50)); assert_eq!(Pools::api_pending_rewards(20), Some(50)); @@ -1430,7 +1442,7 @@ mod claim_payout { assert_eq!(Pools::api_pending_rewards(20), Some(50)); assert_eq!(Pools::api_pending_rewards(30), Some(0)); - Balances::mutate_account(&default_reward_account(), |f| f.free += 60).unwrap(); + deposit_rewards(60); assert_eq!(Pools::api_pending_rewards(10), Some(30 + 50 + 20)); assert_eq!(Pools::api_pending_rewards(20), Some(50 + 20)); @@ -1464,7 +1476,7 @@ mod claim_payout { Balances::make_free_balance_be(&30, ed + 20); assert_ok!(Pools::join(RuntimeOrigin::signed(30), 10, 1)); - Balances::mutate_account(&default_reward_account(), |f| f.free += 40).unwrap(); + deposit_rewards(40); // everyone claims. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -1488,7 +1500,7 @@ mod claim_payout { assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(30), BondExtra::FreeBalance(10))); // more rewards come in. - Balances::mutate_account(&default_reward_account(), |f| f.free += 100).unwrap(); + deposit_rewards(100); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); @@ -1514,7 +1526,7 @@ mod claim_payout { Balances::make_free_balance_be(&20, ed + 20); assert_ok!(Pools::join(RuntimeOrigin::signed(20), 20, 1)); - Balances::mutate_account(&default_reward_account(), |f| f.free += 30).unwrap(); + deposit_rewards(30); // everyone claims. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -1535,7 +1547,7 @@ mod claim_payout { assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 10)); // more rewards come in. - Balances::mutate_account(&default_reward_account(), |f| f.free += 100).unwrap(); + deposit_rewards(100); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); @@ -1562,7 +1574,7 @@ mod claim_payout { assert_ok!(Pools::join(RuntimeOrigin::signed(30), 10, 1)); // 10 gets 10, 20 gets 20, 30 gets 10 - Balances::mutate_account(&default_reward_account(), |f| f.free += 40).unwrap(); + deposit_rewards(40); // some claim. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -1581,7 +1593,7 @@ mod claim_payout { ); // 10 gets 20, 20 gets 40, 30 gets 20 - Balances::mutate_account(&default_reward_account(), |f| f.free += 80).unwrap(); + deposit_rewards(80); // some claim. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -1596,7 +1608,7 @@ mod claim_payout { ); // 10 gets 20, 20 gets 40, 30 gets 20 - Balances::mutate_account(&default_reward_account(), |f| f.free += 80).unwrap(); + deposit_rewards(80); // some claim. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -1629,7 +1641,7 @@ mod claim_payout { assert_ok!(Pools::join(RuntimeOrigin::signed(20), 20, 1)); // 10 gets 10, 20 gets 20, 30 gets 10 - Balances::mutate_account(&default_reward_account(), |f| f.free += 30).unwrap(); + deposit_rewards(30); // some claim. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -1645,7 +1657,7 @@ mod claim_payout { ); // 20 has not claimed yet, more reward comes - Balances::mutate_account(&default_reward_account(), |f| f.free += 60).unwrap(); + deposit_rewards(60); // and 20 bonds more -- they should not have more share of this reward. assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(20), BondExtra::FreeBalance(10))); @@ -1665,7 +1677,7 @@ mod claim_payout { ); // but in the next round of rewards, the extra10 they bonded has an impact. - Balances::mutate_account(&default_reward_account(), |f| f.free += 60).unwrap(); + deposit_rewards(60); // everyone claim. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -1695,7 +1707,7 @@ mod claim_payout { assert_eq!(member_10.last_recorded_reward_counter, 0.into()); // transfer some reward to pool 1. - Balances::mutate_account(&default_reward_account(), |f| f.free += 60).unwrap(); + deposit_rewards(60); // create pool 2 Balances::make_free_balance_be(&20, 100); @@ -1779,7 +1791,7 @@ mod claim_payout { } // transfer some reward to pool 1. - Balances::mutate_account(&default_reward_account(), |f| f.free += 60).unwrap(); + deposit_rewards(60); { join(30, 10); @@ -1849,7 +1861,7 @@ mod claim_payout { // 10 bonds extra again with some rewards. This reward should be split equally between // 10 and 20, as they both have equal points now. - Balances::mutate_account(&default_reward_account(), |f| f.free += 30).unwrap(); + deposit_rewards(30); { assert_ok!(Pools::bond_extra( @@ -1905,7 +1917,7 @@ mod claim_payout { MaxPoolMembersPerPool::::set(None); // pool receives some rewards. - Balances::mutate_account(&default_reward_account(), |f| f.free += 30).unwrap(); + deposit_rewards(30); System::reset_events(); // 10 cashes it out, and bonds it. @@ -1975,7 +1987,7 @@ mod claim_payout { } // some rewards come in. - Balances::mutate_account(&default_reward_account(), |f| f.free += 30).unwrap(); + deposit_rewards(30); // and 30 also unbonds half. { @@ -2058,7 +2070,7 @@ mod claim_payout { ); // some rewards come in. - Balances::mutate_account(&default_reward_account(), |f| f.free += 40).unwrap(); + deposit_rewards(40); // everyone claims assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -2120,8 +2132,7 @@ mod claim_payout { .build_and_execute(|| { // some rewards come in. assert_eq!(Balances::free_balance(&default_reward_account()), unit); - Balances::mutate_account(&default_reward_account(), |f| f.free += unit / 1000) - .unwrap(); + deposit_rewards(unit / 1000); // everyone claims assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -3329,11 +3340,7 @@ mod withdraw_unbonded { ); assert_eq!( balances_events_since_last_call(), - vec![BEvent::BalanceSet { - who: default_bonded_account(), - free: 300, - reserved: 0 - }] + vec![BEvent::BalanceSet { who: default_bonded_account(), free: 300 }] ); // When @@ -3447,11 +3454,7 @@ mod withdraw_unbonded { ); assert_eq!( balances_events_since_last_call(), - vec![BEvent::BalanceSet { - who: default_bonded_account(), - free: 300, - reserved: 0 - },] + vec![BEvent::BalanceSet { who: default_bonded_account(), free: 300 },] ); CurrentEra::set(StakingMock::bonding_duration()); @@ -5078,18 +5081,15 @@ mod reward_counter_precision { let expected_smallest_reward = inflation(50) / 10u128.pow(18); // tad bit less. cannot be paid out. - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += - expected_smallest_reward - 1)); + deposit_rewards(expected_smallest_reward - 1); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_eq!(pool_events_since_last_call(), vec![]); // revert it. - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free -= - expected_smallest_reward - 1)); + remove_rewards(expected_smallest_reward - 1); // tad bit more. can be claimed. - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += - expected_smallest_reward + 1)); + deposit_rewards(expected_smallest_reward + 1); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_eq!( pool_events_since_last_call(), @@ -5114,9 +5114,7 @@ mod reward_counter_precision { assert_ok!(Pools::join(RuntimeOrigin::signed(20), tiny_bond / 2, 1)); // Suddenly, add a shit ton of rewards. - assert_ok!( - Balances::mutate_account(&default_reward_account(), |a| a.free += inflation(1)) - ); + deposit_rewards(inflation(1)); // now claim. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -5155,8 +5153,7 @@ mod reward_counter_precision { // is earning all of the inflation per year (which is really unrealistic, but worse // case), that will be: let pool_total_earnings_10_years = inflation(10) - POLKADOT_TOTAL_ISSUANCE_GENESIS; - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += - pool_total_earnings_10_years)); + deposit_rewards(pool_total_earnings_10_years); // some whale now joins with the other half ot the total issuance. This will bloat all // the calculation regarding current reward counter. @@ -5186,7 +5183,7 @@ mod reward_counter_precision { assert_ok!(Pools::join(RuntimeOrigin::signed(30), 10 * DOT, 1)); // and give a reasonably small reward to the pool. - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += DOT)); + deposit_rewards(DOT); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); assert_eq!( @@ -5258,9 +5255,7 @@ mod reward_counter_precision { assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10 * DOT, 1)); // earn some small rewards - assert_ok!( - Balances::mutate_account(&default_reward_account(), |a| a.free += DOT / 1000) - ); + deposit_rewards(DOT / 1000); // no point in claiming for 20 (nonetheless, it should be harmless) assert!(pending_rewards(20).unwrap().is_zero()); @@ -5280,9 +5275,7 @@ mod reward_counter_precision { // earn some small more, still nothing can be claimed for 20, but 10 claims their // share. - assert_ok!( - Balances::mutate_account(&default_reward_account(), |a| a.free += DOT / 1000) - ); + deposit_rewards(DOT / 1000); assert!(pending_rewards(20).unwrap().is_zero()); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_eq!( @@ -5291,9 +5284,7 @@ mod reward_counter_precision { ); // earn some more rewards, this time 20 can also claim. - assert_ok!( - Balances::mutate_account(&default_reward_account(), |a| a.free += DOT / 1000) - ); + deposit_rewards(DOT / 1000); assert_eq!(pending_rewards(20).unwrap(), 1); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); @@ -5332,9 +5323,7 @@ mod reward_counter_precision { assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10 * DOT, 1)); // earn some small rewards - assert_ok!( - Balances::mutate_account(&default_reward_account(), |a| a.free += DOT / 1000) - ); + deposit_rewards(DOT / 1000); // if 20 claims now, their reward counter should stay the same, so that they have a // chance of claiming this if they let it accumulate. Also see @@ -5442,7 +5431,7 @@ mod commission { // Pool earns 80 points and a payout is triggered. // Given: - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 80)); + deposit_rewards(80); assert_eq!( PoolMembers::::get(10).unwrap(), PoolMember:: { pool_id, points: 10, ..Default::default() } @@ -5489,7 +5478,7 @@ mod commission { // is next called, which is not done in this test segment.. // Given: - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 100)); + deposit_rewards(100); // When: assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -5625,7 +5614,7 @@ mod commission { 1, Some((Perbill::from_percent(10), root)), )); - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 40)); + deposit_rewards(40); // When: assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -6377,7 +6366,7 @@ mod commission { ); // The pool earns 10 points - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 10)); + deposit_rewards(10); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then: @@ -6387,7 +6376,7 @@ mod commission { ); // The pool earns 17 points - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 17)); + deposit_rewards(17); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then: @@ -6397,7 +6386,7 @@ mod commission { ); // The pool earns 50 points - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 50)); + deposit_rewards(50); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then: @@ -6407,7 +6396,7 @@ mod commission { ); // The pool earns 10439 points - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 10439)); + deposit_rewards(10439); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then: @@ -6427,7 +6416,7 @@ mod commission { )); // Given: - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 200)); + deposit_rewards(200); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then: @@ -6469,7 +6458,7 @@ mod commission { // When: // The pool earns 100 points - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 100)); + deposit_rewards(100); // Change commission to 20% assert_ok!(Pools::set_commission( @@ -6486,7 +6475,7 @@ mod commission { ); // The pool earns 100 points - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 100)); + deposit_rewards(100); // Then: @@ -6535,7 +6524,7 @@ mod commission { // When: // The pool earns 100 points - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 100)); + deposit_rewards(100); // Claim payout: assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); @@ -6592,7 +6581,7 @@ mod commission { ); // The pool earns 10 points - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 10)); + deposit_rewards(10); // execute the payout assert_ok!(Pools::do_reward_payout( @@ -6634,7 +6623,7 @@ mod commission { ); // The pool earns 10 points - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 10)); + deposit_rewards(10); // execute the payout assert_ok!(Pools::do_reward_payout( @@ -6677,7 +6666,7 @@ mod commission { ); // Pool earns 80 points, payout is triggered. - assert_ok!(Balances::mutate_account(&default_reward_account(), |a| a.free += 80)); + deposit_rewards(80); assert_eq!( PoolMembers::::get(10).unwrap(), PoolMember:: { pool_id, points: 10, ..Default::default() } diff --git a/frame/nomination-pools/test-staking/src/mock.rs b/frame/nomination-pools/test-staking/src/mock.rs index 0550c8e0ca..9726f5e6da 100644 --- a/frame/nomination-pools/test-staking/src/mock.rs +++ b/frame/nomination-pools/test-staking/src/mock.rs @@ -86,6 +86,10 @@ impl pallet_balances::Config for Runtime { type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } pallet_staking_reward_curve::build! { diff --git a/frame/offences/benchmarking/src/mock.rs b/frame/offences/benchmarking/src/mock.rs index 233aa449d3..058361e9f7 100644 --- a/frame/offences/benchmarking/src/mock.rs +++ b/frame/offences/benchmarking/src/mock.rs @@ -74,6 +74,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<10>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } impl pallet_timestamp::Config for Test { diff --git a/frame/preimage/src/mock.rs b/frame/preimage/src/mock.rs index 23875ccb0e..5054a77a81 100644 --- a/frame/preimage/src/mock.rs +++ b/frame/preimage/src/mock.rs @@ -84,6 +84,10 @@ impl pallet_balances::Config for Test { type MaxLocks = (); type MaxReserves = ConstU32<50>; type ReserveIdentifier = [u8; 8]; + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } ord_parameter_types! { diff --git a/frame/proxy/src/tests.rs b/frame/proxy/src/tests.rs index c49c344aca..f3771083c4 100644 --- a/frame/proxy/src/tests.rs +++ b/frame/proxy/src/tests.rs @@ -88,6 +88,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } impl pallet_utility::Config for Test { type RuntimeEvent = RuntimeEvent; @@ -124,7 +128,10 @@ impl InstanceFilter for ProxyType { match self { ProxyType::Any => true, ProxyType::JustTransfer => { - matches!(c, RuntimeCall::Balances(pallet_balances::Call::transfer { .. })) + matches!( + c, + RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { .. }) + ) }, ProxyType::JustUtility => matches!(c, RuntimeCall::Utility { .. }), } @@ -161,7 +168,7 @@ impl Config for Test { use super::{Call as ProxyCall, Event as ProxyEvent}; use frame_system::Call as SystemCall; -use pallet_balances::{Call as BalancesCall, Error as BalancesError, Event as BalancesEvent}; +use pallet_balances::{Call as BalancesCall, Event as BalancesEvent}; use pallet_utility::{Call as UtilityCall, Event as UtilityEvent}; type SystemError = frame_system::Error; @@ -169,7 +176,7 @@ type SystemError = frame_system::Error; pub fn new_test_ext() -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); pallet_balances::GenesisConfig:: { - balances: vec![(1, 10), (2, 10), (3, 10), (4, 10), (5, 2)], + balances: vec![(1, 10), (2, 10), (3, 10), (4, 10), (5, 3)], } .assimilate_storage(&mut t) .unwrap(); @@ -193,7 +200,7 @@ fn expect_events(e: Vec) { } fn call_transfer(dest: u64, value: u64) -> RuntimeCall { - RuntimeCall::Balances(BalancesCall::transfer { dest, value }) + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest, value }) } #[test] @@ -345,7 +352,7 @@ fn proxy_announced_removes_announcement_and_returns_deposit() { #[test] fn filtering_works() { new_test_ext().execute_with(|| { - assert!(Balances::mutate_account(&1, |a| a.free = 1000).is_ok()); + Balances::make_free_balance_be(&1, 1000); assert_ok!(Proxy::add_proxy(RuntimeOrigin::signed(1), 2, ProxyType::Any, 0)); assert_ok!(Proxy::add_proxy(RuntimeOrigin::signed(1), 3, ProxyType::JustTransfer, 0)); assert_ok!(Proxy::add_proxy(RuntimeOrigin::signed(1), 4, ProxyType::JustUtility, 0)); @@ -361,7 +368,7 @@ fn filtering_works() { ); let derivative_id = Utility::derivative_account_id(1, 0); - assert!(Balances::mutate_account(&derivative_id, |a| a.free = 1000).is_ok()); + Balances::make_free_balance_be(&derivative_id, 1000); let inner = Box::new(call_transfer(6, 1)); let call = Box::new(RuntimeCall::Utility(UtilityCall::as_derivative { @@ -516,7 +523,7 @@ fn cannot_add_proxy_without_balance() { assert_eq!(Balances::reserved_balance(5), 2); assert_noop!( Proxy::add_proxy(RuntimeOrigin::signed(5), 4, ProxyType::Any, 0), - BalancesError::::InsufficientBalance + DispatchError::ConsumerRemaining, ); }); } @@ -564,6 +571,7 @@ fn proxying_works() { #[test] fn pure_works() { new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 11); // An extra one for the ED. assert_ok!(Proxy::create_pure(RuntimeOrigin::signed(1), ProxyType::Any, 0, 0)); let anon = Proxy::pure_account(&1, &ProxyType::Any, 0, None); System::assert_last_event( @@ -592,7 +600,7 @@ fn pure_works() { assert_ok!(Proxy::create_pure(RuntimeOrigin::signed(1), ProxyType::Any, 0, 0)); let call = Box::new(call_transfer(6, 1)); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(3), anon, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(3), anon, 5)); assert_ok!(Proxy::proxy(RuntimeOrigin::signed(1), anon, None, call)); System::assert_last_event(ProxyEvent::ProxyExecuted { result: Ok(()) }.into()); assert_eq!(Balances::free_balance(6), 1); @@ -611,9 +619,9 @@ fn pure_works() { Proxy::kill_pure(RuntimeOrigin::signed(1), 1, ProxyType::Any, 0, 1, 0), Error::::NoPermission ); - assert_eq!(Balances::free_balance(1), 0); + assert_eq!(Balances::free_balance(1), 1); assert_ok!(Proxy::proxy(RuntimeOrigin::signed(1), anon, None, call.clone())); - assert_eq!(Balances::free_balance(1), 2); + assert_eq!(Balances::free_balance(1), 3); assert_noop!( Proxy::proxy(RuntimeOrigin::signed(1), anon, None, call.clone()), Error::::NotProxy diff --git a/frame/recovery/src/mock.rs b/frame/recovery/src/mock.rs index 1d21be801c..5c190e2a24 100644 --- a/frame/recovery/src/mock.rs +++ b/frame/recovery/src/mock.rs @@ -86,6 +86,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } parameter_types! { diff --git a/frame/recovery/src/tests.rs b/frame/recovery/src/tests.rs index 9381d6a881..85024d0bc4 100644 --- a/frame/recovery/src/tests.rs +++ b/frame/recovery/src/tests.rs @@ -45,7 +45,10 @@ fn set_recovered_works() { // Root can set a recovered account though assert_ok!(Recovery::set_recovered(RuntimeOrigin::root(), 5, 1)); // Account 1 should now be able to make a call through account 5 - let call = Box::new(RuntimeCall::Balances(BalancesCall::transfer { dest: 1, value: 100 })); + let call = Box::new(RuntimeCall::Balances(BalancesCall::transfer_allow_death { + dest: 1, + value: 100, + })); assert_ok!(Recovery::as_recovered(RuntimeOrigin::signed(1), 5, call)); // Account 1 has successfully drained the funds from account 5 assert_eq!(Balances::free_balance(1), 200); @@ -93,7 +96,10 @@ fn recovery_life_cycle_works() { assert_ok!(Recovery::as_recovered(RuntimeOrigin::signed(1), 5, call)); // Account 1 should now be able to make a call through account 5 to get all of their funds assert_eq!(Balances::free_balance(5), 110); - let call = Box::new(RuntimeCall::Balances(BalancesCall::transfer { dest: 1, value: 110 })); + let call = Box::new(RuntimeCall::Balances(BalancesCall::transfer_allow_death { + dest: 1, + value: 110, + })); assert_ok!(Recovery::as_recovered(RuntimeOrigin::signed(1), 5, call)); // All funds have been fully recovered! assert_eq!(Balances::free_balance(1), 200); diff --git a/frame/referenda/src/migration.rs b/frame/referenda/src/migration.rs index e15eb499b4..c27ab452ac 100644 --- a/frame/referenda/src/migration.rs +++ b/frame/referenda/src/migration.rs @@ -191,7 +191,7 @@ pub mod test { #[test] pub fn referendum_status_v0() { // make sure the bytes of the encoded referendum v0 is decodable. - let ongoing_encoded = sp_core::Bytes::from_str("0x00000000013001012a000000000000000400000100000000000000010000000000000001000000000000000a00000000000000000000000000000000000100").unwrap(); + let ongoing_encoded = sp_core::Bytes::from_str("0x00000000012c01082a0000000000000004000100000000000000010000000000000001000000000000000a00000000000000000000000000000000000100").unwrap(); let ongoing_dec = v0::ReferendumInfoOf::::decode(&mut &*ongoing_encoded).unwrap(); let ongoing = v0::ReferendumInfoOf::::Ongoing(create_status_v0()); assert_eq!(ongoing, ongoing_dec); diff --git a/frame/referenda/src/mock.rs b/frame/referenda/src/mock.rs index ade2a09a9f..cdedb79556 100644 --- a/frame/referenda/src/mock.rs +++ b/frame/referenda/src/mock.rs @@ -57,7 +57,7 @@ frame_support::construct_runtime!( pub struct BaseFilter; impl Contains for BaseFilter { fn contains(call: &RuntimeCall) -> bool { - !matches!(call, &RuntimeCall::Balances(pallet_balances::Call::set_balance { .. })) + !matches!(call, &RuntimeCall::Balances(pallet_balances::Call::force_set_balance { .. })) } } @@ -120,6 +120,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } parameter_types! { pub static AlarmInterval: u64 = 1; @@ -295,19 +299,14 @@ impl VoteTally for Tally { } pub fn set_balance_proposal(value: u64) -> Vec { - RuntimeCall::Balances(pallet_balances::Call::set_balance { - who: 42, - new_free: value, - new_reserved: 0, - }) - .encode() + RuntimeCall::Balances(pallet_balances::Call::force_set_balance { who: 42, new_free: value }) + .encode() } pub fn set_balance_proposal_bounded(value: u64) -> BoundedCallOf { - let c = RuntimeCall::Balances(pallet_balances::Call::set_balance { + let c = RuntimeCall::Balances(pallet_balances::Call::force_set_balance { who: 42, new_free: value, - new_reserved: 0, }); ::bound(c).unwrap() } diff --git a/frame/root-offences/src/mock.rs b/frame/root-offences/src/mock.rs index 0937c43d6e..828551e4d9 100644 --- a/frame/root-offences/src/mock.rs +++ b/frame/root-offences/src/mock.rs @@ -121,6 +121,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } pallet_staking_reward_curve::build! { diff --git a/frame/scored-pool/src/mock.rs b/frame/scored-pool/src/mock.rs index a91b1a60f4..f10a1320ef 100644 --- a/frame/scored-pool/src/mock.rs +++ b/frame/scored-pool/src/mock.rs @@ -91,6 +91,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } parameter_types! { diff --git a/frame/session/benchmarking/src/mock.rs b/frame/session/benchmarking/src/mock.rs index 4c4accbbfa..b7671255f6 100644 --- a/frame/session/benchmarking/src/mock.rs +++ b/frame/session/benchmarking/src/mock.rs @@ -84,6 +84,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<10>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } impl pallet_timestamp::Config for Test { diff --git a/frame/society/src/mock.rs b/frame/society/src/mock.rs index 73565dedc7..9f72febc21 100644 --- a/frame/society/src/mock.rs +++ b/frame/society/src/mock.rs @@ -93,6 +93,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } impl Config for Test { diff --git a/frame/staking/src/mock.rs b/frame/staking/src/mock.rs index e876940cb9..c2f559a978 100644 --- a/frame/staking/src/mock.rs +++ b/frame/staking/src/mock.rs @@ -157,6 +157,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } sp_runtime::impl_opaque_keys! { diff --git a/frame/staking/src/tests.rs b/frame/staking/src/tests.rs index 966eae8c1d..36cafcc185 100644 --- a/frame/staking/src/tests.rs +++ b/frame/staking/src/tests.rs @@ -30,7 +30,7 @@ use pallet_balances::Error as BalancesError; use sp_runtime::{ assert_eq_error_rate, traits::{BadOrigin, Dispatchable}, - Perbill, Percent, Rounding, + Perbill, Percent, Rounding, TokenError, }; use sp_staking::{ offence::{DisableStrategy, OffenceDetails, OnOffenceHandler}, @@ -98,8 +98,8 @@ fn force_unstake_works() { add_slash(&11); // Cant transfer assert_noop!( - Balances::transfer(RuntimeOrigin::signed(11), 1, 10), - BalancesError::::LiquidityRestrictions + Balances::transfer_allow_death(RuntimeOrigin::signed(11), 1, 10), + TokenError::Frozen, ); // Force unstake requires root. assert_noop!(Staking::force_unstake(RuntimeOrigin::signed(11), 11, 2), BadOrigin); @@ -113,7 +113,7 @@ fn force_unstake_works() { // No longer bonded. assert_eq!(Staking::bonded(&11), None); // Transfer works. - assert_ok!(Balances::transfer(RuntimeOrigin::signed(11), 1, 10)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(11), 1, 10)); }); } @@ -960,14 +960,14 @@ fn cannot_transfer_staked_balance() { assert_eq!(Staking::eras_stakers(active_era(), 11).total, 1000); // Confirm account 11 cannot transfer as a result assert_noop!( - Balances::transfer(RuntimeOrigin::signed(11), 20, 1), - BalancesError::::LiquidityRestrictions + Balances::transfer_allow_death(RuntimeOrigin::signed(11), 20, 1), + TokenError::Frozen, ); // Give account 11 extra free balance let _ = Balances::make_free_balance_be(&11, 10000); // Confirm that account 11 can now transfer some balance - assert_ok!(Balances::transfer(RuntimeOrigin::signed(11), 20, 1)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(11), 20, 1)); }); } @@ -985,10 +985,10 @@ fn cannot_transfer_staked_balance_2() { assert_eq!(Staking::eras_stakers(active_era(), 21).total, 1000); // Confirm account 21 can transfer at most 1000 assert_noop!( - Balances::transfer(RuntimeOrigin::signed(21), 20, 1001), - BalancesError::::LiquidityRestrictions + Balances::transfer_allow_death(RuntimeOrigin::signed(21), 20, 1001), + TokenError::Frozen, ); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(21), 20, 1000)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(21), 20, 1000)); }); } @@ -4187,7 +4187,7 @@ fn payout_creates_controller() { bond_nominator(1234, 1337, 100, vec![11]); // kill controller - assert_ok!(Balances::transfer(RuntimeOrigin::signed(1337), 1234, 100)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(1337), 1234, 100)); assert_eq!(Balances::free_balance(1337), 0); mock::start_active_era(1); diff --git a/frame/state-trie-migration/src/lib.rs b/frame/state-trie-migration/src/lib.rs index 5385c6b5f4..1f6266d999 100644 --- a/frame/state-trie-migration/src/lib.rs +++ b/frame/state-trie-migration/src/lib.rs @@ -1128,6 +1128,10 @@ mod mock { type MaxReserves = (); type ReserveIdentifier = [u8; 8]; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } /// Test only Weights for state migration. diff --git a/frame/support/src/traits/stored_map.rs b/frame/support/src/traits/stored_map.rs index a073b26825..cbe70f2932 100644 --- a/frame/support/src/traits/stored_map.rs +++ b/frame/support/src/traits/stored_map.rs @@ -17,7 +17,7 @@ //! Traits and associated datatypes for managing abstract stored values. -use crate::{storage::StorageMap, traits::misc::HandleLifetime}; +use crate::storage::StorageMap; use codec::FullCodec; use sp_runtime::DispatchError; @@ -81,48 +81,29 @@ pub trait StoredMap { /// be the default value), or where the account is being removed or reset back to the default value /// where previously it did exist (though may have been in a default state). This works well with /// system module's `CallOnCreatedAccount` and `CallKillAccount`. -pub struct StorageMapShim(sp_std::marker::PhantomData<(S, L, K, T)>); -impl< - S: StorageMap, - L: HandleLifetime, - K: FullCodec, - T: FullCodec + Default, - > StoredMap for StorageMapShim +pub struct StorageMapShim(sp_std::marker::PhantomData<(S, K, T)>); +impl, K: FullCodec, T: FullCodec + Default> StoredMap + for StorageMapShim { fn get(k: &K) -> T { S::get(k) } fn insert(k: &K, t: T) -> Result<(), DispatchError> { - if !S::contains_key(&k) { - L::created(k)?; - } S::insert(k, t); Ok(()) } fn remove(k: &K) -> Result<(), DispatchError> { if S::contains_key(&k) { - L::killed(k)?; S::remove(k); } Ok(()) } fn mutate(k: &K, f: impl FnOnce(&mut T) -> R) -> Result { - if !S::contains_key(&k) { - L::created(k)?; - } Ok(S::mutate(k, f)) } fn mutate_exists(k: &K, f: impl FnOnce(&mut Option) -> R) -> Result { S::try_mutate_exists(k, |maybe_value| { - let existed = maybe_value.is_some(); let r = f(maybe_value); - let exists = maybe_value.is_some(); - - if !existed && exists { - L::created(k)?; - } else if existed && !exists { - L::killed(k)?; - } Ok(r) }) } @@ -131,15 +112,7 @@ impl< f: impl FnOnce(&mut Option) -> Result, ) -> Result { S::try_mutate_exists(k, |maybe_value| { - let existed = maybe_value.is_some(); let r = f(maybe_value)?; - let exists = maybe_value.is_some(); - - if !existed && exists { - L::created(k).map_err(E::from)?; - } else if existed && !exists { - L::killed(k).map_err(E::from)?; - } Ok(r) }) } diff --git a/frame/support/src/traits/tokens.rs b/frame/support/src/traits/tokens.rs index 2d2bd63ada..d2753caa6a 100644 --- a/frame/support/src/traits/tokens.rs +++ b/frame/support/src/traits/tokens.rs @@ -30,6 +30,7 @@ pub use imbalance::Imbalance; pub mod pay; pub use misc::{ AssetId, Balance, BalanceConversion, BalanceStatus, ConvertRank, DepositConsequence, - ExistenceRequirement, GetSalary, Locker, WithdrawConsequence, WithdrawReasons, + ExistenceRequirement, Fortitude, GetSalary, Locker, Precision, Preservation, Provenance, + Restriction, WithdrawConsequence, WithdrawReasons, }; pub use pay::{Pay, PayFromAccount, PaymentStatus}; diff --git a/frame/support/src/traits/tokens/fungible.rs b/frame/support/src/traits/tokens/fungible.rs deleted file mode 100644 index 12bac29727..0000000000 --- a/frame/support/src/traits/tokens/fungible.rs +++ /dev/null @@ -1,367 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! The traits for dealing with a single fungible token class and any associated types. - -use super::{ - misc::{Balance, DepositConsequence, WithdrawConsequence}, - *, -}; -use crate::{ - dispatch::{DispatchError, DispatchResult}, - traits::misc::Get, -}; -use sp_runtime::traits::Saturating; - -mod balanced; -mod imbalance; -pub use balanced::{Balanced, Unbalanced}; -pub use imbalance::{CreditOf, DebtOf, HandleImbalanceDrop, Imbalance}; - -/// Trait for providing balance-inspection access to a fungible asset. -pub trait Inspect { - /// Scalar type for representing balance of an account. - type Balance: Balance; - - /// The total amount of issuance in the system. - fn total_issuance() -> Self::Balance; - - /// The total amount of issuance in the system excluding those which are controlled by the - /// system. - fn active_issuance() -> Self::Balance { - Self::total_issuance() - } - - /// The minimum balance any single account may have. - fn minimum_balance() -> Self::Balance; - - /// Get the balance of `who`. - fn balance(who: &AccountId) -> Self::Balance; - - /// Get the maximum amount that `who` can withdraw/transfer successfully. - fn reducible_balance(who: &AccountId, keep_alive: bool) -> Self::Balance; - - /// Returns `true` if the balance of `who` may be increased by `amount`. - /// - /// - `who`: The account of which the balance should be increased by `amount`. - /// - `amount`: How much should the balance be increased? - /// - `mint`: Will `amount` be minted to deposit it into `account`? - fn can_deposit(who: &AccountId, amount: Self::Balance, mint: bool) -> DepositConsequence; - - /// Returns `Failed` if the balance of `who` may not be decreased by `amount`, otherwise - /// the consequence. - fn can_withdraw(who: &AccountId, amount: Self::Balance) -> WithdrawConsequence; -} - -/// Trait for providing an ERC-20 style fungible asset. -pub trait Mutate: Inspect { - /// Increase the balance of `who` by exactly `amount`, minting new tokens. If that isn't - /// possible then an `Err` is returned and nothing is changed. - fn mint_into(who: &AccountId, amount: Self::Balance) -> DispatchResult; - - /// Decrease the balance of `who` by at least `amount`, possibly slightly more in the case of - /// minimum_balance requirements, burning the tokens. If that isn't possible then an `Err` is - /// returned and nothing is changed. If successful, the amount of tokens reduced is returned. - fn burn_from(who: &AccountId, amount: Self::Balance) -> Result; - - // TODO: Remove. - /// Attempt to reduce the balance of `who` by as much as possible up to `amount`, and possibly - /// slightly more due to minimum_balance requirements. If no decrease is possible then an `Err` - /// is returned and nothing is changed. If successful, the amount of tokens reduced is returned. - /// - /// The default implementation just uses `withdraw` along with `reducible_balance` to ensure - /// that it doesn't fail. - fn slash(who: &AccountId, amount: Self::Balance) -> Result { - Self::burn_from(who, Self::reducible_balance(who, false).min(amount)) - } - - /// Transfer funds from one account into another. The default implementation uses `mint_into` - /// and `burn_from` and may generate unwanted events. - fn teleport( - source: &AccountId, - dest: &AccountId, - amount: Self::Balance, - ) -> Result { - let extra = Self::can_withdraw(&source, amount).into_result()?; - // As we first burn and then mint, we don't need to check if `mint` fits into the supply. - // If we can withdraw/burn it, we can also mint it again. - Self::can_deposit(dest, amount.saturating_add(extra), false).into_result()?; - let actual = Self::burn_from(source, amount)?; - debug_assert!( - actual == amount.saturating_add(extra), - "can_withdraw must agree with withdraw; qed" - ); - match Self::mint_into(dest, actual) { - Ok(_) => Ok(actual), - Err(err) => { - debug_assert!(false, "can_deposit returned true previously; qed"); - // attempt to return the funds back to source - let revert = Self::mint_into(source, actual); - debug_assert!(revert.is_ok(), "withdrew funds previously; qed"); - Err(err) - }, - } - } -} - -/// Trait for providing a fungible asset which can only be transferred. -pub trait Transfer: Inspect { - /// Transfer funds from one account into another. - fn transfer( - source: &AccountId, - dest: &AccountId, - amount: Self::Balance, - keep_alive: bool, - ) -> Result; - - /// Reduce the active issuance by some amount. - fn deactivate(_: Self::Balance) {} - - /// Increase the active issuance by some amount, up to the outstanding amount reduced. - fn reactivate(_: Self::Balance) {} -} - -/// Trait for inspecting a fungible asset which can be reserved. -pub trait InspectHold: Inspect { - /// Amount of funds held in reserve by `who`. - fn balance_on_hold(who: &AccountId) -> Self::Balance; - - /// Check to see if some `amount` of funds of `who` may be placed on hold. - fn can_hold(who: &AccountId, amount: Self::Balance) -> bool; -} - -// TODO: Introduce `HoldReason`. -/// Trait for mutating a fungible asset which can be reserved. -pub trait MutateHold: InspectHold + Transfer { - /// Hold some funds in an account. - fn hold(who: &AccountId, amount: Self::Balance) -> DispatchResult; - - /// Release up to `amount` held funds in an account. - /// - /// The actual amount released is returned with `Ok`. - /// - /// If `best_effort` is `true`, then the amount actually unreserved and returned as the inner - /// value of `Ok` may be smaller than the `amount` passed. - fn release( - who: &AccountId, - amount: Self::Balance, - best_effort: bool, - ) -> Result; - - // TODO: Introduce repatriate_held - - /// Transfer held funds into a destination account. - /// - /// If `on_hold` is `true`, then the destination account must already exist and the assets - /// transferred will still be on hold in the destination account. If not, then the destination - /// account need not already exist, but must be creatable. - /// - /// If `best_effort` is `true`, then an amount less than `amount` may be transferred without - /// error. - /// - /// The actual amount transferred is returned, or `Err` in the case of error and nothing is - /// changed. - fn transfer_held( - source: &AccountId, - dest: &AccountId, - amount: Self::Balance, - best_effort: bool, - on_held: bool, - ) -> Result; -} - -/// Trait for slashing a fungible asset which can be reserved. -pub trait BalancedHold: Balanced + MutateHold { - /// Reduce the balance of some funds on hold in an account. - /// - /// The resulting imbalance is the first item of the tuple returned. - /// - /// As much funds that are on hold up to `amount` will be deducted as possible. If this is less - /// than `amount`, then a non-zero second item will be returned. - fn slash_held( - who: &AccountId, - amount: Self::Balance, - ) -> (CreditOf, Self::Balance); -} - -impl + MutateHold> BalancedHold for T { - // TODO: This should be implemented properly, and `slash` should be removed. - fn slash_held( - who: &AccountId, - amount: Self::Balance, - ) -> (CreditOf, Self::Balance) { - let actual = match Self::release(who, amount, true) { - Ok(x) => x, - Err(_) => return (Imbalance::default(), amount), - }; - >::slash(who, actual) - } -} - -/// Convert a `fungibles` trait implementation into a `fungible` trait implementation by identifying -/// a single item. -pub struct ItemOf< - F: fungibles::Inspect, - A: Get<>::AssetId>, - AccountId, ->(sp_std::marker::PhantomData<(F, A, AccountId)>); - -impl< - F: fungibles::Inspect, - A: Get<>::AssetId>, - AccountId, - > Inspect for ItemOf -{ - type Balance = >::Balance; - fn total_issuance() -> Self::Balance { - >::total_issuance(A::get()) - } - fn active_issuance() -> Self::Balance { - >::active_issuance(A::get()) - } - fn minimum_balance() -> Self::Balance { - >::minimum_balance(A::get()) - } - fn balance(who: &AccountId) -> Self::Balance { - >::balance(A::get(), who) - } - fn reducible_balance(who: &AccountId, keep_alive: bool) -> Self::Balance { - >::reducible_balance(A::get(), who, keep_alive) - } - fn can_deposit(who: &AccountId, amount: Self::Balance, mint: bool) -> DepositConsequence { - >::can_deposit(A::get(), who, amount, mint) - } - fn can_withdraw(who: &AccountId, amount: Self::Balance) -> WithdrawConsequence { - >::can_withdraw(A::get(), who, amount) - } -} - -impl< - F: fungibles::Mutate, - A: Get<>::AssetId>, - AccountId, - > Mutate for ItemOf -{ - fn mint_into(who: &AccountId, amount: Self::Balance) -> DispatchResult { - >::mint_into(A::get(), who, amount) - } - fn burn_from(who: &AccountId, amount: Self::Balance) -> Result { - >::burn_from(A::get(), who, amount) - } -} - -impl< - F: fungibles::Transfer, - A: Get<>::AssetId>, - AccountId, - > Transfer for ItemOf -{ - fn transfer( - source: &AccountId, - dest: &AccountId, - amount: Self::Balance, - keep_alive: bool, - ) -> Result { - >::transfer(A::get(), source, dest, amount, keep_alive) - } - fn deactivate(amount: Self::Balance) { - >::deactivate(A::get(), amount) - } - fn reactivate(amount: Self::Balance) { - >::reactivate(A::get(), amount) - } -} - -impl< - F: fungibles::InspectHold, - A: Get<>::AssetId>, - AccountId, - > InspectHold for ItemOf -{ - fn balance_on_hold(who: &AccountId) -> Self::Balance { - >::balance_on_hold(A::get(), who) - } - fn can_hold(who: &AccountId, amount: Self::Balance) -> bool { - >::can_hold(A::get(), who, amount) - } -} - -impl< - F: fungibles::MutateHold, - A: Get<>::AssetId>, - AccountId, - > MutateHold for ItemOf -{ - fn hold(who: &AccountId, amount: Self::Balance) -> DispatchResult { - >::hold(A::get(), who, amount) - } - fn release( - who: &AccountId, - amount: Self::Balance, - best_effort: bool, - ) -> Result { - >::release(A::get(), who, amount, best_effort) - } - fn transfer_held( - source: &AccountId, - dest: &AccountId, - amount: Self::Balance, - best_effort: bool, - on_hold: bool, - ) -> Result { - >::transfer_held( - A::get(), - source, - dest, - amount, - best_effort, - on_hold, - ) - } -} - -impl< - F: fungibles::Unbalanced, - A: Get<>::AssetId>, - AccountId, - > Unbalanced for ItemOf -{ - fn set_balance(who: &AccountId, amount: Self::Balance) -> DispatchResult { - >::set_balance(A::get(), who, amount) - } - fn set_total_issuance(amount: Self::Balance) -> () { - >::set_total_issuance(A::get(), amount) - } - fn decrease_balance( - who: &AccountId, - amount: Self::Balance, - ) -> Result { - >::decrease_balance(A::get(), who, amount) - } - fn decrease_balance_at_most(who: &AccountId, amount: Self::Balance) -> Self::Balance { - >::decrease_balance_at_most(A::get(), who, amount) - } - fn increase_balance( - who: &AccountId, - amount: Self::Balance, - ) -> Result { - >::increase_balance(A::get(), who, amount) - } - fn increase_balance_at_most(who: &AccountId, amount: Self::Balance) -> Self::Balance { - >::increase_balance_at_most(A::get(), who, amount) - } -} diff --git a/frame/support/src/traits/tokens/fungible/balanced.rs b/frame/support/src/traits/tokens/fungible/balanced.rs deleted file mode 100644 index 437d2c82de..0000000000 --- a/frame/support/src/traits/tokens/fungible/balanced.rs +++ /dev/null @@ -1,354 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! The trait and associated types for sets of fungible tokens that manage total issuance without -//! requiring atomic balanced operations. - -use super::{super::Imbalance as ImbalanceT, *}; -use crate::{ - dispatch::{DispatchError, DispatchResult}, - traits::misc::{SameOrOther, TryDrop}, -}; -use sp_runtime::{ - traits::{CheckedAdd, Zero}, - ArithmeticError, TokenError, -}; -use sp_std::marker::PhantomData; - -/// 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. -pub trait Balanced: Inspect { - /// The type for managing what happens when an instance of `Debt` is dropped without being used. - type OnDropDebt: HandleImbalanceDrop; - /// The type for managing what happens when an instance of `Credit` is dropped without being - /// used. - 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`. - /// - /// This is infallible, but doesn't guarantee that the entire `amount` is burnt, for example - /// in the case of underflow. - fn rescind(amount: Self::Balance) -> DebtOf; - - /// 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`. - /// - /// This is infallible, but doesn't guarantee that the entire `amount` is issued, for example - /// in the case of overflow. - fn issue(amount: Self::Balance) -> CreditOf; - - /// Produce a pair of imbalances that cancel each other out exactly. - /// - /// 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) -> (DebtOf, CreditOf) { - (Self::rescind(amount), Self::issue(amount)) - } - - /// Deducts up to `value` from the combined balance of `who`. This function cannot fail. - /// - /// The resulting imbalance is the first item of the tuple returned. - /// - /// As much funds up to `value` will be deducted as possible. If this is less than `value`, - /// then a non-zero second item will be returned. - fn slash(who: &AccountId, amount: Self::Balance) -> (CreditOf, Self::Balance); - - /// Mints exactly `value` into the account of `who`. - /// - /// If `who` doesn't exist, nothing is done and an `Err` returned. This could happen because it - /// the account doesn't yet exist and it isn't possible to create it under the current - /// circumstances and with `value` in it. - fn deposit( - who: &AccountId, - value: Self::Balance, - ) -> Result, DispatchError>; - - /// Removes `value` balance from `who` account if possible. - /// - /// If the removal is not possible, then it returns `Err` and nothing is changed. - /// - /// If the operation is successful, this will return `Ok` with a `NegativeImbalance` whose value - /// is no less than `value`. It may be more in the case that removing it reduced it below - /// `Self::minimum_balance()`. - fn withdraw( - who: &AccountId, - value: Self::Balance, - // TODO: liveness: ExistenceRequirement, - ) -> Result, DispatchError>; - - /// The balance of `who` is increased in order to counter `credit`. If the whole of `credit` - /// 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 - /// already exist for this to succeed. - fn resolve( - who: &AccountId, - credit: CreditOf, - ) -> Result<(), CreditOf> { - let v = credit.peek(); - let debt = match Self::deposit(who, v) { - Err(_) => return Err(credit), - Ok(d) => d, - }; - let result = credit.offset(debt).try_drop(); - debug_assert!(result.is_ok(), "ok deposit return must be equal to credit value; qed"); - Ok(()) - } - - /// The balance of `who` is decreased in order to counter `debt`. If the whole of `debt` - /// cannot be countered, then nothing is changed and the original `debt` is returned in an - /// `Err`. - fn settle( - who: &AccountId, - debt: DebtOf, - // TODO: liveness: ExistenceRequirement, - ) -> Result, DebtOf> { - let amount = debt.peek(); - let credit = match Self::withdraw(who, amount) { - Err(_) => return Err(debt), - Ok(d) => d, - }; - match credit.offset(debt) { - SameOrOther::None => Ok(CreditOf::::zero()), - SameOrOther::Same(dust) => Ok(dust), - SameOrOther::Other(rest) => { - debug_assert!(false, "ok withdraw return must be at least debt value; qed"); - Err(rest) - }, - } - } -} - -/// A fungible token class where the balance can be set arbitrarily. -/// -/// **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 -/// for the underlying datatype to implement so the user gets the much safer `Balanced` trait to -/// use. -pub trait Unbalanced: Inspect { - /// Set the balance of `who` to `amount`. If this cannot be done for some reason (e.g. - /// because the account cannot be created or an overflow) then an `Err` is returned. - fn set_balance(who: &AccountId, amount: Self::Balance) -> DispatchResult; - - /// Set the total issuance to `amount`. - fn set_total_issuance(amount: Self::Balance); - - /// Reduce the balance of `who` by `amount`. If it cannot be reduced by that amount for - /// some reason, return `Err` and don't reduce it at all. If Ok, return the imbalance. - /// - /// Minimum balance will be respected and the returned imbalance may be up to - /// `Self::minimum_balance() - 1` greater than `amount`. - fn decrease_balance( - who: &AccountId, - amount: Self::Balance, - ) -> Result { - let old_balance = Self::balance(who); - let (mut new_balance, mut amount) = if Self::reducible_balance(who, false) < amount { - return Err(TokenError::NoFunds.into()) - } else { - (old_balance - amount, amount) - }; - if new_balance < Self::minimum_balance() { - amount = amount.saturating_add(new_balance); - new_balance = Zero::zero(); - } - // Defensive only - this should not fail now. - Self::set_balance(who, new_balance)?; - Ok(amount) - } - - /// Reduce the balance of `who` by the most that is possible, up to `amount`. - /// - /// Minimum balance will be respected and the returned imbalance may be up to - /// `Self::minimum_balance() - 1` greater than `amount`. - /// - /// Return the imbalance by which the account was reduced. - fn decrease_balance_at_most(who: &AccountId, amount: Self::Balance) -> Self::Balance { - let old_balance = Self::balance(who); - let old_free_balance = Self::reducible_balance(who, false); - let (mut new_balance, mut amount) = if old_free_balance < amount { - (old_balance.saturating_sub(old_free_balance), old_free_balance) - } else { - (old_balance - amount, amount) - }; - let minimum_balance = Self::minimum_balance(); - if new_balance < minimum_balance { - amount = amount.saturating_add(new_balance); - new_balance = Zero::zero(); - } - let mut r = Self::set_balance(who, new_balance); - if r.is_err() { - // Some error, probably because we tried to destroy an account which cannot be - // destroyed. - if new_balance.is_zero() && amount >= minimum_balance { - new_balance = minimum_balance; - amount -= minimum_balance; - r = Self::set_balance(who, new_balance); - } - if r.is_err() { - // Still an error. Apparently it's not possible to reduce at all. - amount = Zero::zero(); - } - } - amount - } - - /// Increase the balance of `who` by `amount`. If it cannot be increased by that amount - /// for some reason, return `Err` and don't increase it at all. If Ok, return the imbalance. - /// - /// Minimum balance will be respected and an error will be returned if - /// `amount < Self::minimum_balance()` when the account of `who` is zero. - fn increase_balance( - who: &AccountId, - amount: Self::Balance, - ) -> Result { - let old_balance = Self::balance(who); - let new_balance = old_balance.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; - if new_balance < Self::minimum_balance() { - return Err(TokenError::BelowMinimum.into()) - } - if old_balance != new_balance { - Self::set_balance(who, new_balance)?; - } - Ok(amount) - } - - /// Increase the balance of `who` by the most that is possible, up to `amount`. - /// - /// Minimum balance will be respected and the returned imbalance will be zero in the case that - /// `amount < Self::minimum_balance()`. - /// - /// Return the imbalance by which the account was increased. - fn increase_balance_at_most(who: &AccountId, amount: Self::Balance) -> Self::Balance { - let old_balance = Self::balance(who); - let mut new_balance = old_balance.saturating_add(amount); - let mut amount = new_balance - old_balance; - if new_balance < Self::minimum_balance() { - new_balance = Zero::zero(); - amount = Zero::zero(); - } - if old_balance == new_balance || Self::set_balance(who, new_balance).is_ok() { - amount - } else { - Zero::zero() - } - } -} - -/// Simple handler for an imbalance drop which increases the total issuance of the system by the -/// imbalance amount. Used for leftover debt. -pub struct IncreaseIssuance(PhantomData<(AccountId, U)>); -impl> HandleImbalanceDrop - for IncreaseIssuance -{ - fn handle(amount: U::Balance) { - U::set_total_issuance(U::total_issuance().saturating_add(amount)) - } -} - -/// Simple handler for an imbalance drop which decreases the total issuance of the system by the -/// imbalance amount. Used for leftover credit. -pub struct DecreaseIssuance(PhantomData<(AccountId, U)>); -impl> HandleImbalanceDrop - for DecreaseIssuance -{ - fn handle(amount: U::Balance) { - U::set_total_issuance(U::total_issuance().saturating_sub(amount)) - } -} - -/// An imbalance type which uses `DecreaseIssuance` to deal with anything `Drop`ed. -/// -/// Basically means that funds in someone's account have been removed and not yet placed anywhere -/// else. If it gets dropped, then those funds will be assumed to be "burned" and the total supply -/// will be accordingly decreased to ensure it equals the sum of the balances of all accounts. -type Credit = Imbalance< - >::Balance, - DecreaseIssuance, - IncreaseIssuance, ->; - -/// An imbalance type which uses `IncreaseIssuance` to deal with anything `Drop`ed. -/// -/// Basically means that there are funds in someone's account whose origin is as yet unaccounted -/// for. If it gets dropped, then those funds will be assumed to be "minted" and the total supply -/// will be accordingly increased to ensure it equals the sum of the balances of all accounts. -type Debt = Imbalance< - >::Balance, - IncreaseIssuance, - DecreaseIssuance, ->; - -/// Create some `Credit` item. Only for internal use. -fn credit>(amount: U::Balance) -> Credit { - Imbalance::new(amount) -} - -/// Create some `Debt` item. Only for internal use. -fn debt>(amount: U::Balance) -> Debt { - Imbalance::new(amount) -} - -impl> Balanced for U { - type OnDropCredit = DecreaseIssuance; - type OnDropDebt = IncreaseIssuance; - fn rescind(amount: Self::Balance) -> Debt { - let old = U::total_issuance(); - let new = old.saturating_sub(amount); - U::set_total_issuance(new); - debt(old - new) - } - fn issue(amount: Self::Balance) -> Credit { - let old = U::total_issuance(); - let new = old.saturating_add(amount); - U::set_total_issuance(new); - credit(new - old) - } - fn slash(who: &AccountId, amount: Self::Balance) -> (Credit, Self::Balance) { - let slashed = U::decrease_balance_at_most(who, amount); - // `slashed` could be less than, greater than or equal to `amount`. - // If slashed == amount, it means the account had at least amount in it and it could all be - // removed without a problem. - // If slashed > amount, it means the account had more than amount in it, but not enough more - // to push it over minimum_balance. - // If slashed < amount, it means the account didn't have enough in it to be reduced by - // `amount` without being destroyed. - (credit(slashed), amount.saturating_sub(slashed)) - } - fn deposit( - who: &AccountId, - amount: Self::Balance, - ) -> Result, DispatchError> { - let increase = U::increase_balance(who, amount)?; - Ok(debt(increase)) - } - fn withdraw( - who: &AccountId, - amount: Self::Balance, - // TODO: liveness: ExistenceRequirement, - ) -> Result, DispatchError> { - let decrease = U::decrease_balance(who, amount)?; - Ok(credit(decrease)) - } -} diff --git a/frame/support/src/traits/tokens/fungible/freeze.rs b/frame/support/src/traits/tokens/fungible/freeze.rs new file mode 100644 index 0000000000..1ec3a5fadf --- /dev/null +++ b/frame/support/src/traits/tokens/fungible/freeze.rs @@ -0,0 +1,68 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! The traits for putting freezes within a single fungible token class. + +use scale_info::TypeInfo; +use sp_runtime::DispatchResult; + +/// Trait for inspecting a fungible asset which can be frozen. Freezing is essentially setting a +/// minimum balance bellow which the total balance (inclusive of any funds placed on hold) may not +/// be normally allowed to drop. Generally, freezers will provide an "update" function such that +/// if the total balance does drop below the limit, then the freezer can update their housekeeping +/// accordingly. +pub trait Inspect: super::Inspect { + /// An identifier for a freeze. + type Id: codec::Encode + TypeInfo + 'static; + + /// Amount of funds held in reserve by `who` for the given `id`. + fn balance_frozen(id: &Self::Id, who: &AccountId) -> Self::Balance; + + /// The amount of the balance which can become frozen. Defaults to `total_balance()`. + fn balance_freezable(who: &AccountId) -> Self::Balance { + Self::total_balance(who) + } + + /// Returns `true` if it's possible to introduce a freeze for the given `id` onto the + /// account of `who`. This will be true as long as the implementor supports as many + /// concurrent freeze locks as there are possible values of `id`. + fn can_freeze(id: &Self::Id, who: &AccountId) -> bool; +} + +/// Trait for introducing, altering and removing locks to freeze an account's funds so they never +/// go below a set minimum. +pub trait Mutate: Inspect { + /// Prevent actions which would reduce the balance of the account of `who` below the given + /// `amount` and identify this restriction though the given `id`. Unlike `extend_freeze`, any + /// outstanding freeze in place for `who` under the `id` are dropped. + /// + /// If `amount` is zero, it is equivalent to using `thaw`. + /// + /// Note that `amount` can be greater than the total balance, if desired. + fn set_freeze(id: &Self::Id, who: &AccountId, amount: Self::Balance) -> DispatchResult; + + /// Prevent the balance of the account of `who` from being reduced below the given `amount` and + /// identify this restriction though the given `id`. Unlike `set_freeze`, this does not + /// counteract any pre-existing freezes in place for `who` under the `id`. Also unlike + /// `set_freeze`, in the case that `amount` is zero, this is no-op and never fails. + /// + /// Note that more funds can be locked than the total balance, if desired. + fn extend_freeze(id: &Self::Id, who: &AccountId, amount: Self::Balance) -> DispatchResult; + + /// Remove an existing lock. + fn thaw(id: &Self::Id, who: &AccountId) -> DispatchResult; +} diff --git a/frame/support/src/traits/tokens/fungible/hold.rs b/frame/support/src/traits/tokens/fungible/hold.rs new file mode 100644 index 0000000000..ddcb8c6ac1 --- /dev/null +++ b/frame/support/src/traits/tokens/fungible/hold.rs @@ -0,0 +1,393 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! The traits for putting holds within a single fungible token class. + +use crate::{ + ensure, + traits::tokens::{ + DepositConsequence::Success, + Fortitude::{self, Force}, + Precision::{self, BestEffort, Exact}, + Preservation::{self, Protect}, + Provenance::Extant, + Restriction::{self, Free, OnHold}, + }, +}; +use scale_info::TypeInfo; +use sp_arithmetic::{ + traits::{CheckedAdd, CheckedSub, Zero}, + ArithmeticError, +}; +use sp_runtime::{DispatchError, DispatchResult, Saturating, TokenError}; + +use super::*; + +/// Trait for inspecting a fungible asset whose accounts support partitioning and slashing. +pub trait Inspect: super::Inspect { + /// An identifier for a hold. Used for disambiguating different holds so that + /// they can be individually replaced or removed and funds from one hold don't accidentally + /// become unreserved or slashed for another. + type Reason: codec::Encode + TypeInfo + 'static; + + /// Amount of funds on hold (for all hold reasons) of `who`. + fn total_balance_on_hold(who: &AccountId) -> Self::Balance; + + /// Get the maximum amount that the `total_balance_on_hold` of `who` can be reduced successfully + /// based on whether we are willing to force the reduction and potentially go below user-level + /// restrictions on the minimum amount of the account. Note: This cannot bring the account into + /// an inconsistent state with regards any required existential deposit. + /// + /// Always less than `total_balance_on_hold()`. + fn reducible_total_balance_on_hold(who: &AccountId, force: Fortitude) -> Self::Balance; + + /// Amount of funds on hold (for the given reason) of `who`. + fn balance_on_hold(reason: &Self::Reason, who: &AccountId) -> Self::Balance; + + /// Returns `true` if it's possible to place (additional) funds under a hold of a given + /// `reason`. This may fail if the account has exhausted a limited number of concurrent + /// holds or if it cannot be made to exist (e.g. there is no provider reference). + /// + /// NOTE: This does not take into account changes which could be made to the account of `who` + /// (such as removing a provider reference) after this call is made. Any usage of this should + /// therefore ensure the account is already in the appropriate state prior to calling it. + fn hold_available(reason: &Self::Reason, who: &AccountId) -> bool; + + /// Check to see if some `amount` of funds of `who` may be placed on hold with the given + /// `reason`. Reasons why this may not be true: + /// + /// - The implementor supports only a limited number of concurrent holds on an account which is + /// the possible values of `reason`; + /// - The total balance of the account is less than `amount`; + /// - Removing `amount` from the total balance would kill the account and remove the only + /// provider reference. + /// + /// Note: we pass `true` as the third argument to `reducible_balance` since we assume that if + /// needed the balance can slashed. If we are using a simple non-forcing reserve-transfer, then + /// we really ought to check that we are not reducing the funds below the freeze-limit (if any). + /// + /// NOTE: This does not take into account changes which could be made to the account of `who` + /// (such as removing a provider reference) after this call is made. Any usage of this should + /// therefore ensure the account is already in the appropriate state prior to calling it. + fn ensure_can_hold( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + ) -> DispatchResult { + ensure!(Self::hold_available(reason, who), TokenError::CannotCreateHold); + ensure!( + amount <= Self::reducible_balance(who, Protect, Force), + TokenError::FundsUnavailable + ); + Ok(()) + } + + /// Check to see if some `amount` of funds of `who` may be placed on hold for the given + /// `reason`. Reasons why this may not be true: + /// + /// - The implementor supports only a limited number of concurrernt holds on an account which is + /// the possible values of `reason`; + /// - The main balance of the account is less than `amount`; + /// - Removing `amount` from the main balance would kill the account and remove the only + /// provider reference. + /// + /// NOTE: This does not take into account changes which could be made to the account of `who` + /// (such as removing a provider reference) after this call is made. Any usage of this should + /// therefore ensure the account is already in the appropriate state prior to calling it. + fn can_hold(reason: &Self::Reason, who: &AccountId, amount: Self::Balance) -> bool { + Self::ensure_can_hold(reason, who, amount).is_ok() + } +} + +/// A fungible, holdable token class where the balance on hold can be set arbitrarily. +/// +/// **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 +/// for the underlying datatype to implement so the user gets the much safer `Balanced` trait to +/// use. +pub trait Unbalanced: Inspect { + /// Forcefully set the balance on hold of `who` to `amount`. This is independent of any other + /// balances on hold or the main ("free") balance. + /// + /// If this call executes successfully, you can `assert_eq!(Self::balance_on_hold(), amount);`. + /// + /// This function does its best to force the balance change through, but will not break system + /// invariants such as any Existential Deposits needed or overflows/underflows. + /// If this cannot be done for some reason (e.g. because the account doesn't exist) then an + /// `Err` is returned. + // Implmentation note: This should increment the consumer refs if it moves total on hold from + // zero to non-zero and decrement in the opposite direction. + // + // Since this was not done in the previous logic, this will need either a migration or a + // state item which tracks whether the account is on the old logic or new. + fn set_balance_on_hold( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + ) -> DispatchResult; + + /// Reduce the balance on hold 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 + /// 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. + fn decrease_balance_on_hold( + reason: &Self::Reason, + who: &AccountId, + mut amount: Self::Balance, + precision: Precision, + ) -> Result { + let old_balance = Self::balance_on_hold(reason, who); + if let BestEffort = precision { + amount = amount.min(old_balance); + } + let new_balance = old_balance.checked_sub(&amount).ok_or(TokenError::FundsUnavailable)?; + Self::set_balance_on_hold(reason, who, new_balance)?; + Ok(amount) + } + + /// Increase the balance on hold of `who` by `amount`. + /// + /// If it cannot be increased by that amount for some reason, return `Err` and don't increase + /// it at all. If Ok, return the imbalance. + fn increase_balance_on_hold( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + let old_balance = Self::balance_on_hold(reason, who); + let new_balance = if let BestEffort = precision { + old_balance.saturating_add(amount) + } else { + old_balance.checked_add(&amount).ok_or(ArithmeticError::Overflow)? + }; + let amount = new_balance.saturating_sub(old_balance); + if !amount.is_zero() { + Self::set_balance_on_hold(reason, who, new_balance)?; + } + Ok(amount) + } +} + +/// Trait for mutating a fungible asset which can be placed on hold. +pub trait Mutate: + Inspect + super::Unbalanced + Unbalanced +{ + /// Hold some funds in an account. If a hold for `reason` is already in place, then this + /// will increase it. + fn hold(reason: &Self::Reason, who: &AccountId, amount: Self::Balance) -> DispatchResult { + // NOTE: This doesn't change the total balance of the account so there's no need to + // check liquidity. + + Self::ensure_can_hold(reason, who, amount)?; + // Should be infallible now, but we proceed softly anyway. + Self::decrease_balance(who, amount, Exact, Protect, Force)?; + Self::increase_balance_on_hold(reason, who, amount, BestEffort)?; + Self::done_hold(reason, who, amount); + Ok(()) + } + + /// Release up to `amount` held funds in an account. + /// + /// The actual amount released is returned with `Ok`. + /// + /// If `precision` is `BestEffort`, then the amount actually unreserved and returned as the + /// inner value of `Ok` may be smaller than the `amount` passed. + /// + /// NOTE! The inner of the `Ok` result variant returns the *actual* amount released. This is the + /// opposite of the `ReservableCurrency::unreserve()` result, which gives the amount not able + /// to be released! + fn release( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + // NOTE: This doesn't change the total balance of the account so there's no need to + // check liquidity. + + // We want to make sure we can deposit the amount in advance. If we can't then something is + // very wrong. + ensure!(Self::can_deposit(who, amount, Extant) == Success, TokenError::CannotCreate); + // Get the amount we can actually take from the hold. This might be less than what we want + // if we're only doing a best-effort. + let amount = Self::decrease_balance_on_hold(reason, who, amount, precision)?; + // Increase the main balance by what we took. We always do a best-effort here because we + // already checked that we can deposit before. + let actual = Self::increase_balance(who, amount, BestEffort)?; + Self::done_release(reason, who, actual); + Ok(actual) + } + + /// Attempt to decrease the balance of `who` which is held for the given `reason` by `amount`. + /// + /// If `precision` is `BestEffort`, then as much as possible is reduced, up to `amount`, and the + /// amount of tokens reduced is returned. Otherwise, if the total amount can be reduced, then it + /// is and the amount returned, and if not, then nothing changes and `Err` is returned. + /// + /// If `force` is `Force`, then locks/freezes will be ignored. This should only be used when + /// conducting slashing or other activity which materially disadvantages the account holder + /// since it could provide a means of circumventing freezes. + fn burn_held( + reason: &Self::Reason, + who: &AccountId, + mut amount: Self::Balance, + precision: Precision, + force: Fortitude, + ) -> Result { + // We must check total-balance requirements if `!force`. + let liquid = Self::reducible_total_balance_on_hold(who, force); + if let BestEffort = precision { + amount = amount.min(liquid); + } else { + ensure!(amount <= liquid, TokenError::Frozen); + } + let amount = Self::decrease_balance_on_hold(reason, who, amount, precision)?; + Self::set_total_issuance(Self::total_issuance().saturating_sub(amount)); + Self::done_burn_held(reason, who, amount); + Ok(amount) + } + + /// Transfer held funds into a destination account. + /// + /// If `on_hold` is `true`, then the destination account must already exist and the assets + /// transferred will still be on hold in the destination account. If not, then the destination + /// account need not already exist, but must be creatable. + /// + /// If `precision` is `BestEffort`, then an amount less than `amount` may be transferred without + /// error. + /// + /// If `force` is `Force`, then other fund-locking mechanisms may be disregarded. It should be + /// left as `Polite` in most circumstances, but when you want the same power as a `slash`, it + /// may be `Force`. + /// + /// The actual amount transferred is returned, or `Err` in the case of error and nothing is + /// changed. + fn transfer_on_hold( + reason: &Self::Reason, + source: &AccountId, + dest: &AccountId, + mut amount: Self::Balance, + precision: Precision, + mode: Restriction, + force: Fortitude, + ) -> Result { + // We must check total-balance requirements if `force` is `Fortitude::Polite`. + let have = Self::balance_on_hold(reason, source); + let liquid = Self::reducible_total_balance_on_hold(source, force); + if let BestEffort = precision { + amount = amount.min(liquid).min(have); + } else { + ensure!(amount <= liquid, TokenError::Frozen); + ensure!(amount <= have, TokenError::FundsUnavailable); + } + + // We want to make sure we can deposit the amount in advance. If we can't then something is + // very wrong. + ensure!(Self::can_deposit(dest, amount, Extant) == Success, TokenError::CannotCreate); + ensure!(mode == Free || Self::hold_available(reason, dest), TokenError::CannotCreateHold); + + let amount = Self::decrease_balance_on_hold(reason, source, amount, precision)?; + let actual = if mode == OnHold { + Self::increase_balance_on_hold(reason, dest, amount, precision)? + } else { + Self::increase_balance(dest, amount, precision)? + }; + Self::done_transfer_on_hold(reason, source, dest, actual); + Ok(actual) + } + + /// Transfer some `amount` of free balance from `source` to become owned by `dest` but on hold + /// for `reason`. + /// + /// If `precision` is `BestEffort`, then an amount less than `amount` may be transferred without + /// error. + /// + /// `source` must obey the requirements of `keep_alive`. + /// + /// If `force` is `Force`, then other fund-locking mechanisms may be disregarded. It should be + /// left as `Polite` in most circumstances, but when you want the same power as a `slash`, it + /// may be `Force`. + /// + /// The amount placed on hold is returned or `Err` in the case of error and nothing is changed. + /// + /// WARNING: This may return an error after a partial storage mutation. It should be used only + /// inside a transactional storage context and an `Err` result must imply a storage rollback. + fn transfer_and_hold( + reason: &Self::Reason, + source: &AccountId, + dest: &AccountId, + amount: Self::Balance, + precision: Precision, + expendability: Preservation, + force: Fortitude, + ) -> Result { + ensure!(Self::hold_available(reason, dest), TokenError::CannotCreateHold); + ensure!(Self::can_deposit(dest, amount, Extant) == Success, TokenError::CannotCreate); + let actual = Self::decrease_balance(source, amount, precision, expendability, force)?; + Self::increase_balance_on_hold(reason, dest, actual, precision)?; + Self::done_transfer_on_hold(reason, source, dest, actual); + Ok(actual) + } + + fn done_hold(_reason: &Self::Reason, _who: &AccountId, _amount: Self::Balance) {} + fn done_release(_reason: &Self::Reason, _who: &AccountId, _amount: Self::Balance) {} + fn done_burn_held(_reason: &Self::Reason, _who: &AccountId, _amount: Self::Balance) {} + fn done_transfer_on_hold( + _reason: &Self::Reason, + _source: &AccountId, + _dest: &AccountId, + _amount: Self::Balance, + ) { + } + fn done_transfer_and_hold( + _reason: &Self::Reason, + _source: &AccountId, + _dest: &AccountId, + _transferred: Self::Balance, + ) { + } +} + +/// Trait for slashing a fungible asset which can be place on hold. +pub trait Balanced: super::Balanced + Unbalanced { + /// Reduce the balance of some funds on hold in an account. + /// + /// The resulting imbalance is the first item of the tuple returned. + /// + /// As much funds that are on hold up to `amount` will be deducted as possible. If this is less + /// than `amount`, then a non-zero second item will be returned. + fn slash( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + ) -> (Credit, Self::Balance) { + let decrease = Self::decrease_balance_on_hold(reason, who, amount, BestEffort) + .unwrap_or(Default::default()); + let credit = + Imbalance::::new(decrease); + Self::done_slash(reason, who, decrease); + (credit, amount.saturating_sub(decrease)) + } + + fn done_slash(_reason: &Self::Reason, _who: &AccountId, _amount: Self::Balance) {} +} diff --git a/frame/support/src/traits/tokens/fungible/imbalance.rs b/frame/support/src/traits/tokens/fungible/imbalance.rs index 1b3d16c62d..de85924a4d 100644 --- a/frame/support/src/traits/tokens/fungible/imbalance.rs +++ b/frame/support/src/traits/tokens/fungible/imbalance.rs @@ -18,8 +18,11 @@ //! The imbalance type and its associates, which handles keeps everything adding up properly with //! unbalanced operations. -use super::{super::Imbalance as ImbalanceT, balanced::Balanced, misc::Balance, *}; -use crate::traits::misc::{SameOrOther, TryDrop}; +use super::{super::Imbalance as ImbalanceT, Balanced, *}; +use crate::traits::{ + misc::{SameOrOther, TryDrop}, + tokens::Balance, +}; use sp_runtime::{traits::Zero, RuntimeDebug}; use sp_std::marker::PhantomData; @@ -30,6 +33,10 @@ pub trait HandleImbalanceDrop { fn handle(amount: Balance); } +impl HandleImbalanceDrop for () { + fn handle(_: Balance) {} +} + /// An imbalance in the system, representing a divergence of recorded token supply from the sum of /// the balances of all accounts. This is `must_use` in order to ensure it gets handled (placing /// into an account, settling from an account or altering the supply). @@ -135,7 +142,7 @@ impl, OppositeOnDrop: HandleImbalance } /// Imbalance implying that the total_issuance value is less than the sum of all account balances. -pub type DebtOf = Imbalance< +pub type Debt = Imbalance< >::Balance, // This will generally be implemented by increasing the total_issuance value. >::OnDropDebt, @@ -144,7 +151,7 @@ pub type DebtOf = Imbalance< /// Imbalance implying that the total_issuance value is greater than the sum of all account /// balances. -pub type CreditOf = Imbalance< +pub type Credit = Imbalance< >::Balance, // This will generally be implemented by decreasing the total_issuance value. >::OnDropCredit, diff --git a/frame/support/src/traits/tokens/fungible/item_of.rs b/frame/support/src/traits/tokens/fungible/item_of.rs new file mode 100644 index 0000000000..cf2d96ef28 --- /dev/null +++ b/frame/support/src/traits/tokens/fungible/item_of.rs @@ -0,0 +1,451 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Adapter to use `fungibles::*` implementations as `fungible::*`. + +use sp_core::Get; +use sp_runtime::{DispatchError, DispatchResult}; + +use super::*; +use crate::traits::tokens::{ + fungibles, DepositConsequence, Fortitude, Imbalance as ImbalanceT, Precision, Preservation, + Provenance, Restriction, WithdrawConsequence, +}; + +/// Convert a `fungibles` trait implementation into a `fungible` trait implementation by identifying +/// a single item. +pub struct ItemOf< + F: fungibles::Inspect, + A: Get<>::AssetId>, + AccountId, +>(sp_std::marker::PhantomData<(F, A, AccountId)>); + +impl< + F: fungibles::Inspect, + A: Get<>::AssetId>, + AccountId, + > Inspect for ItemOf +{ + type Balance = >::Balance; + fn total_issuance() -> Self::Balance { + >::total_issuance(A::get()) + } + fn active_issuance() -> Self::Balance { + >::active_issuance(A::get()) + } + fn minimum_balance() -> Self::Balance { + >::minimum_balance(A::get()) + } + fn balance(who: &AccountId) -> Self::Balance { + >::balance(A::get(), who) + } + fn total_balance(who: &AccountId) -> Self::Balance { + >::total_balance(A::get(), who) + } + fn reducible_balance( + who: &AccountId, + preservation: Preservation, + force: Fortitude, + ) -> Self::Balance { + >::reducible_balance(A::get(), who, preservation, force) + } + fn can_deposit( + who: &AccountId, + amount: Self::Balance, + provenance: Provenance, + ) -> DepositConsequence { + >::can_deposit(A::get(), who, amount, provenance) + } + fn can_withdraw(who: &AccountId, amount: Self::Balance) -> WithdrawConsequence { + >::can_withdraw(A::get(), who, amount) + } +} + +impl< + F: fungibles::InspectHold, + A: Get<>::AssetId>, + AccountId, + > InspectHold for ItemOf +{ + type Reason = F::Reason; + + fn reducible_total_balance_on_hold(who: &AccountId, force: Fortitude) -> Self::Balance { + >::reducible_total_balance_on_hold( + A::get(), + who, + force, + ) + } + fn hold_available(reason: &Self::Reason, who: &AccountId) -> bool { + >::hold_available(A::get(), reason, who) + } + fn total_balance_on_hold(who: &AccountId) -> Self::Balance { + >::total_balance_on_hold(A::get(), who) + } + fn balance_on_hold(reason: &Self::Reason, who: &AccountId) -> Self::Balance { + >::balance_on_hold(A::get(), reason, who) + } + fn can_hold(reason: &Self::Reason, who: &AccountId, amount: Self::Balance) -> bool { + >::can_hold(A::get(), reason, who, amount) + } +} + +impl< + F: fungibles::InspectFreeze, + A: Get<>::AssetId>, + AccountId, + > InspectFreeze for ItemOf +{ + type Id = F::Id; + fn balance_frozen(id: &Self::Id, who: &AccountId) -> Self::Balance { + >::balance_frozen(A::get(), id, who) + } + fn balance_freezable(who: &AccountId) -> Self::Balance { + >::balance_freezable(A::get(), who) + } + fn can_freeze(id: &Self::Id, who: &AccountId) -> bool { + >::can_freeze(A::get(), id, who) + } +} + +impl< + F: fungibles::Unbalanced, + A: Get<>::AssetId>, + AccountId, + > Unbalanced for ItemOf +{ + fn handle_dust(dust: regular::Dust) + where + Self: Sized, + { + >::handle_dust(fungibles::Dust(A::get(), dust.0)) + } + fn write_balance( + who: &AccountId, + amount: Self::Balance, + ) -> Result, DispatchError> { + >::write_balance(A::get(), who, amount) + } + fn set_total_issuance(amount: Self::Balance) -> () { + >::set_total_issuance(A::get(), amount) + } + fn decrease_balance( + who: &AccountId, + amount: Self::Balance, + precision: Precision, + preservation: Preservation, + force: Fortitude, + ) -> Result { + >::decrease_balance( + A::get(), + who, + amount, + precision, + preservation, + force, + ) + } + fn increase_balance( + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + >::increase_balance(A::get(), who, amount, precision) + } +} + +impl< + F: fungibles::UnbalancedHold, + A: Get<>::AssetId>, + AccountId, + > UnbalancedHold for ItemOf +{ + fn set_balance_on_hold( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + ) -> DispatchResult { + >::set_balance_on_hold( + A::get(), + reason, + who, + amount, + ) + } + fn decrease_balance_on_hold( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + >::decrease_balance_on_hold( + A::get(), + reason, + who, + amount, + precision, + ) + } + fn increase_balance_on_hold( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + >::increase_balance_on_hold( + A::get(), + reason, + who, + amount, + precision, + ) + } +} + +impl< + F: fungibles::Mutate, + A: Get<>::AssetId>, + AccountId, + > Mutate for ItemOf +{ + fn mint_into(who: &AccountId, amount: Self::Balance) -> Result { + >::mint_into(A::get(), who, amount) + } + fn burn_from( + who: &AccountId, + amount: Self::Balance, + precision: Precision, + force: Fortitude, + ) -> Result { + >::burn_from(A::get(), who, amount, precision, force) + } + fn shelve(who: &AccountId, amount: Self::Balance) -> Result { + >::shelve(A::get(), who, amount) + } + fn restore(who: &AccountId, amount: Self::Balance) -> Result { + >::restore(A::get(), who, amount) + } + fn transfer( + source: &AccountId, + dest: &AccountId, + amount: Self::Balance, + preservation: Preservation, + ) -> Result { + >::transfer(A::get(), source, dest, amount, preservation) + } + + fn set_balance(who: &AccountId, amount: Self::Balance) -> Self::Balance { + >::set_balance(A::get(), who, amount) + } +} + +impl< + F: fungibles::MutateHold, + A: Get<>::AssetId>, + AccountId, + > MutateHold for ItemOf +{ + fn hold(reason: &Self::Reason, who: &AccountId, amount: Self::Balance) -> DispatchResult { + >::hold(A::get(), reason, who, amount) + } + fn release( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + >::release(A::get(), reason, who, amount, precision) + } + fn burn_held( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + precision: Precision, + force: Fortitude, + ) -> Result { + >::burn_held( + A::get(), + reason, + who, + amount, + precision, + force, + ) + } + fn transfer_on_hold( + reason: &Self::Reason, + source: &AccountId, + dest: &AccountId, + amount: Self::Balance, + precision: Precision, + mode: Restriction, + force: Fortitude, + ) -> Result { + >::transfer_on_hold( + A::get(), + reason, + source, + dest, + amount, + precision, + mode, + force, + ) + } + fn transfer_and_hold( + reason: &Self::Reason, + source: &AccountId, + dest: &AccountId, + amount: Self::Balance, + precision: Precision, + preservation: Preservation, + force: Fortitude, + ) -> Result { + >::transfer_and_hold( + A::get(), + reason, + source, + dest, + amount, + precision, + preservation, + force, + ) + } +} + +impl< + F: fungibles::MutateFreeze, + A: Get<>::AssetId>, + AccountId, + > MutateFreeze for ItemOf +{ + fn set_freeze(id: &Self::Id, who: &AccountId, amount: Self::Balance) -> DispatchResult { + >::set_freeze(A::get(), id, who, amount) + } + fn extend_freeze(id: &Self::Id, who: &AccountId, amount: Self::Balance) -> DispatchResult { + >::extend_freeze(A::get(), id, who, amount) + } + fn thaw(id: &Self::Id, who: &AccountId) -> DispatchResult { + >::thaw(A::get(), id, who) + } +} + +pub struct ConvertImbalanceDropHandler( + sp_std::marker::PhantomData<(AccountId, Balance, AssetIdType, AssetId, Handler)>, +); + +impl< + AccountId, + Balance, + AssetIdType, + AssetId: Get, + Handler: crate::traits::tokens::fungibles::HandleImbalanceDrop, + > HandleImbalanceDrop + for ConvertImbalanceDropHandler +{ + fn handle(amount: Balance) { + Handler::handle(AssetId::get(), amount) + } +} + +impl< + F: fungibles::Inspect + + fungibles::Unbalanced + + fungibles::Balanced, + A: Get<>::AssetId>, + AccountId, + > Balanced for ItemOf +{ + type OnDropDebt = + ConvertImbalanceDropHandler; + type OnDropCredit = + ConvertImbalanceDropHandler; + fn deposit( + who: &AccountId, + value: Self::Balance, + precision: Precision, + ) -> Result, DispatchError> { + >::deposit(A::get(), who, value, precision) + .map(|debt| Imbalance::new(debt.peek())) + } + fn issue(amount: Self::Balance) -> Credit { + Imbalance::new(>::issue(A::get(), amount).peek()) + } + fn pair(amount: Self::Balance) -> (Debt, Credit) { + let (a, b) = >::pair(A::get(), amount); + (Imbalance::new(a.peek()), Imbalance::new(b.peek())) + } + fn rescind(amount: Self::Balance) -> Debt { + Imbalance::new(>::rescind(A::get(), amount).peek()) + } + fn resolve( + who: &AccountId, + credit: Credit, + ) -> Result<(), Credit> { + let credit = fungibles::Imbalance::new(A::get(), credit.peek()); + >::resolve(who, credit) + .map_err(|credit| Imbalance::new(credit.peek())) + } + fn settle( + who: &AccountId, + debt: Debt, + preservation: Preservation, + ) -> Result, Debt> { + let debt = fungibles::Imbalance::new(A::get(), debt.peek()); + >::settle(who, debt, preservation) + .map(|credit| Imbalance::new(credit.peek())) + .map_err(|debt| Imbalance::new(debt.peek())) + } + fn withdraw( + who: &AccountId, + value: Self::Balance, + precision: Precision, + preservation: Preservation, + force: Fortitude, + ) -> Result, DispatchError> { + >::withdraw( + A::get(), + who, + value, + precision, + preservation, + force, + ) + .map(|credit| Imbalance::new(credit.peek())) + } +} + +impl< + F: fungibles::BalancedHold, + A: Get<>::AssetId>, + AccountId, + > BalancedHold for ItemOf +{ + fn slash( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + ) -> (Credit, Self::Balance) { + let (credit, amount) = + >::slash(A::get(), reason, who, amount); + (Imbalance::new(credit.peek()), amount) + } +} + +#[test] +fn test() {} diff --git a/frame/support/src/traits/tokens/fungible/mod.rs b/frame/support/src/traits/tokens/fungible/mod.rs new file mode 100644 index 0000000000..204b85ac29 --- /dev/null +++ b/frame/support/src/traits/tokens/fungible/mod.rs @@ -0,0 +1,56 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! The traits for dealing with a single fungible token class and any associated types. +//! +//! ### User-implememted traits +//! - `Inspect`: Regular balance inspector functions. +//! - `Unbalanced`: Low-level balance mutating functions. Does not guarantee proper book-keeping and +//! so should not be called into directly from application code. Other traits depend on this and +//! provide default implementations based on it. +//! - `UnbalancedHold`: Low-level balance mutating functions for balances placed on hold. Does not +//! guarantee proper book-keeping and so should not be called into directly from application code. +//! Other traits depend on this and provide default implementations based on it. +//! - `Mutate`: Regular balance mutator functions. Pre-implemented using `Unbalanced`, though the +//! `done_*` functions should likely be reimplemented in case you want to do something following +//! the operation such as emit events. +//! - `InspectHold`: Inspector functions for balances on hold. +//! - `MutateHold`: Mutator functions for balances on hold. Mostly pre-implemented using +//! `UnbalancedHold`. +//! - `InspectFreeze`: Inspector functions for frozen balance. +//! - `MutateFreeze`: Mutator functions for frozen balance. +//! - `Balanced`: One-sided mutator functions for regular balances, which return imbalance objects +//! which guarantee eventual book-keeping. May be useful for some sophisticated operations where +//! funds must be removed from an account before it is known precisely what should be done with +//! them. + +pub mod freeze; +pub mod hold; +mod imbalance; +mod item_of; +mod regular; + +pub use freeze::{Inspect as InspectFreeze, Mutate as MutateFreeze}; +pub use hold::{ + Balanced as BalancedHold, Inspect as InspectHold, Mutate as MutateHold, + Unbalanced as UnbalancedHold, +}; +pub use imbalance::{Credit, Debt, HandleImbalanceDrop, Imbalance}; +pub use item_of::ItemOf; +pub use regular::{ + Balanced, DecreaseIssuance, Dust, IncreaseIssuance, Inspect, Mutate, Unbalanced, +}; diff --git a/frame/support/src/traits/tokens/fungible/regular.rs b/frame/support/src/traits/tokens/fungible/regular.rs new file mode 100644 index 0000000000..574392cac8 --- /dev/null +++ b/frame/support/src/traits/tokens/fungible/regular.rs @@ -0,0 +1,506 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! `Inspect` and `Mutate` traits for working with regular balances. + +use crate::{ + dispatch::DispatchError, + ensure, + traits::{ + tokens::{ + misc::{ + Balance, DepositConsequence, + Fortitude::{self, Force, Polite}, + Precision::{self, BestEffort, Exact}, + Preservation::{self, Expendable}, + Provenance::{self, Extant}, + WithdrawConsequence, + }, + Imbalance as ImbalanceT, + }, + SameOrOther, TryDrop, + }, +}; +use sp_arithmetic::traits::{CheckedAdd, CheckedSub, One}; +use sp_runtime::{traits::Saturating, ArithmeticError, TokenError}; +use sp_std::marker::PhantomData; + +use super::{Credit, Debt, HandleImbalanceDrop, Imbalance}; + +/// Trait for providing balance-inspection access to a fungible asset. +pub trait Inspect: Sized { + /// Scalar type for representing balance of an account. + type Balance: Balance; + + /// The total amount of issuance in the system. + fn total_issuance() -> Self::Balance; + + /// The total amount of issuance in the system excluding those which are controlled by the + /// system. + fn active_issuance() -> Self::Balance { + Self::total_issuance() + } + + /// The minimum balance any single account may have. + fn minimum_balance() -> Self::Balance; + + /// Get the total amount of funds whose ultimate bneficial ownership can be determined as `who`. + /// + /// This may include funds which are wholly inaccessible to `who`, either temporarily or even + /// indefinitely. + /// + /// For the amount of the balance which is currently free to be removed from the account without + /// error, use `reducible_balance`. + /// + /// For the amount of the balance which may eventually be free to be removed from the account, + /// use `balance()`. + fn total_balance(who: &AccountId) -> Self::Balance; + + /// Get the balance of `who` which does not include funds which are exclusively allocated to + /// 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()`. + fn balance(who: &AccountId) -> Self::Balance; + + /// Get the maximum amount that `who` can withdraw/transfer successfully based on whether the + /// account should be kept alive (`preservation`) or whether we are willing to force the + /// reduction and potentially go below user-level restrictions on the minimum amount of the + /// account. + /// + /// Always less than or equal to `balance()`. + fn reducible_balance( + who: &AccountId, + preservation: Preservation, + force: Fortitude, + ) -> Self::Balance; + + /// Returns `true` if the balance of `who` may be increased by `amount`. + /// + /// - `who`: The account of which the balance should be increased by `amount`. + /// - `amount`: How much should the balance be increased? + /// - `provenance`: Will `amount` be minted to deposit it into `account` or is it already in the + /// system? + fn can_deposit( + who: &AccountId, + amount: Self::Balance, + provenance: Provenance, + ) -> DepositConsequence; + + /// Returns `Success` if the balance of `who` may be decreased by `amount`, otherwise + /// the consequence. + fn can_withdraw(who: &AccountId, amount: Self::Balance) -> WithdrawConsequence; +} + +/// Special dust type which can be type-safely converted into a `Credit`. +#[must_use] +pub struct Dust>(pub(crate) T::Balance); + +impl> Dust { + /// Convert `Dust` into an instance of `Credit`. + pub fn into_credit(self) -> Credit { + Credit::::new(self.0) + } +} + +/// A fungible token class where the balance can be set arbitrarily. +/// +/// **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 +/// 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`. + /// + /// 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. + fn handle_dust(dust: Dust); + + /// Forcefully set the balance of `who` to `amount`. + /// + /// If this call executes successfully, you can `assert_eq!(Self::balance(), amount);`. + /// + /// For implementations which include one or more balances on hold, then these are *not* + /// included in the `amount`. + /// + /// This function does its best to force the balance change through, but will not break system + /// invariants such as any Existential Deposits needed or overflows/underflows. + /// If this cannot be done for some reason (e.g. because the account cannot be created, deleted + /// or would overflow) then an `Err` is returned. + /// + /// 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`. + fn write_balance( + who: &AccountId, + amount: Self::Balance, + ) -> Result, DispatchError>; + + /// Set the total issuance to `amount`. + fn set_total_issuance(amount: Self::Balance); + + /// 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 + /// 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. + fn decrease_balance( + who: &AccountId, + mut amount: Self::Balance, + precision: Precision, + preservation: Preservation, + force: Fortitude, + ) -> Result { + let old_balance = Self::balance(who); + let free = Self::reducible_balance(who, preservation, force); + if let BestEffort = precision { + amount = amount.min(free); + } + let new_balance = old_balance.checked_sub(&amount).ok_or(TokenError::FundsUnavailable)?; + if let Some(dust) = Self::write_balance(who, new_balance)? { + Self::handle_dust(Dust(dust)); + } + Ok(old_balance.saturating_sub(new_balance)) + } + + /// Increase the balance of `who` by `amount`. + /// + /// If it cannot be increased by that amount for some reason, return `Err` and don't increase + /// it at all. If Ok, return the imbalance. + /// Minimum balance will be respected and an error will be returned if + /// `amount < Self::minimum_balance()` when the account of `who` is zero. + fn increase_balance( + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + let old_balance = Self::balance(who); + let new_balance = if let BestEffort = precision { + old_balance.saturating_add(amount) + } else { + old_balance.checked_add(&amount).ok_or(ArithmeticError::Overflow)? + }; + if new_balance < Self::minimum_balance() { + // Attempt to increase from 0 to below minimum -> stays at zero. + if let BestEffort = precision { + Ok(Default::default()) + } else { + Err(TokenError::BelowMinimum.into()) + } + } else { + if new_balance == old_balance { + Ok(Default::default()) + } else { + if let Some(dust) = Self::write_balance(who, new_balance)? { + Self::handle_dust(Dust(dust)); + } + Ok(new_balance.saturating_sub(old_balance)) + } + } + } + + /// Reduce the active issuance by some amount. + fn deactivate(_: Self::Balance) {} + + /// Increase the active issuance by some amount, up to the outstanding amount reduced. + fn reactivate(_: Self::Balance) {} +} + +/// Trait for providing a basic fungible asset. +pub trait Mutate: Inspect + Unbalanced { + /// Increase the balance of `who` by exactly `amount`, minting new tokens. If that isn't + /// possible then an `Err` is returned and nothing is changed. + fn mint_into(who: &AccountId, amount: Self::Balance) -> Result { + Self::total_issuance().checked_add(&amount).ok_or(ArithmeticError::Overflow)?; + let actual = Self::increase_balance(who, amount, Exact)?; + Self::set_total_issuance(Self::total_issuance().saturating_add(actual)); + Self::done_mint_into(who, amount); + Ok(actual) + } + + /// Decrease the balance of `who` by at least `amount`, possibly slightly more in the case of + /// minimum-balance requirements, burning the tokens. If that isn't possible then an `Err` is + /// returned and nothing is changed. If successful, the amount of tokens reduced is returned. + fn burn_from( + who: &AccountId, + amount: Self::Balance, + precision: Precision, + force: Fortitude, + ) -> Result { + let actual = Self::reducible_balance(who, Expendable, force).min(amount); + ensure!(actual == amount || precision == BestEffort, TokenError::FundsUnavailable); + Self::total_issuance().checked_sub(&actual).ok_or(ArithmeticError::Overflow)?; + let actual = Self::decrease_balance(who, actual, BestEffort, Expendable, force)?; + Self::set_total_issuance(Self::total_issuance().saturating_sub(actual)); + Self::done_burn_from(who, actual); + Ok(actual) + } + + /// 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 + /// 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). + /// + /// Because of this expectation, any metadata associated with the asset is expected to survive + /// the suspect-resume cycle. + fn shelve(who: &AccountId, amount: Self::Balance) -> Result { + let actual = Self::reducible_balance(who, Expendable, Polite).min(amount); + ensure!(actual == amount, TokenError::FundsUnavailable); + Self::total_issuance().checked_sub(&actual).ok_or(ArithmeticError::Overflow)?; + let actual = Self::decrease_balance(who, actual, BestEffort, Expendable, Polite)?; + Self::set_total_issuance(Self::total_issuance().saturating_sub(actual)); + Self::done_shelve(who, actual); + Ok(actual) + } + + /// 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 + /// 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). + /// + /// Because of this expectation, any metadata associated with the asset is expected to survive + /// the suspect-resume cycle. + fn restore(who: &AccountId, amount: Self::Balance) -> Result { + Self::total_issuance().checked_add(&amount).ok_or(ArithmeticError::Overflow)?; + let actual = Self::increase_balance(who, amount, Exact)?; + Self::set_total_issuance(Self::total_issuance().saturating_add(actual)); + Self::done_restore(who, amount); + Ok(actual) + } + + /// Transfer funds from one account into another. + fn transfer( + source: &AccountId, + dest: &AccountId, + amount: Self::Balance, + preservation: Preservation, + ) -> Result { + let _extra = Self::can_withdraw(source, amount).into_result(preservation != Expendable)?; + Self::can_deposit(dest, amount, Extant).into_result()?; + Self::decrease_balance(source, amount, BestEffort, preservation, Polite)?; + // This should never fail as we checked `can_deposit` earlier. But we do a best-effort + // anyway. + let _ = Self::increase_balance(dest, amount, BestEffort); + Self::done_transfer(source, dest, amount); + Ok(amount) + } + + /// Simple infallible function to force an account to have a particular balance, good for use + /// in tests and benchmarks but not recommended for production code owing to the lack of + /// error reporting. + /// + /// Returns the new balance. + fn set_balance(who: &AccountId, amount: Self::Balance) -> Self::Balance { + let b = Self::balance(who); + if b > amount { + Self::burn_from(who, b - amount, BestEffort, Force).map(|d| amount.saturating_sub(d)) + } else { + Self::mint_into(who, amount - b).map(|d| amount.saturating_add(d)) + } + .unwrap_or(b) + } + + fn done_mint_into(_who: &AccountId, _amount: Self::Balance) {} + fn done_burn_from(_who: &AccountId, _amount: Self::Balance) {} + fn done_shelve(_who: &AccountId, _amount: Self::Balance) {} + fn done_restore(_who: &AccountId, _amount: Self::Balance) {} + fn done_transfer(_source: &AccountId, _dest: &AccountId, _amount: Self::Balance) {} +} + +/// Simple handler for an imbalance drop which increases the total issuance of the system by the +/// imbalance amount. Used for leftover debt. +pub struct IncreaseIssuance(PhantomData<(AccountId, U)>); +impl> HandleImbalanceDrop + for IncreaseIssuance +{ + fn handle(amount: U::Balance) { + U::set_total_issuance(U::total_issuance().saturating_add(amount)) + } +} + +/// Simple handler for an imbalance drop which decreases the total issuance of the system by the +/// imbalance amount. Used for leftover credit. +pub struct DecreaseIssuance(PhantomData<(AccountId, U)>); +impl> HandleImbalanceDrop + for DecreaseIssuance +{ + fn handle(amount: U::Balance) { + U::set_total_issuance(U::total_issuance().saturating_sub(amount)) + } +} + +/// 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. +pub trait Balanced: Inspect + Unbalanced { + /// The type for managing what happens when an instance of `Debt` is dropped without being used. + type OnDropDebt: HandleImbalanceDrop; + /// The type for managing what happens when an instance of `Credit` is dropped without being + /// used. + 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`. + /// + /// This is infallible, but doesn't guarantee that the entire `amount` is burnt, for example + /// in the case of underflow. + fn rescind(amount: Self::Balance) -> Debt { + let old = Self::total_issuance(); + let new = old.saturating_sub(amount); + Self::set_total_issuance(new); + let delta = old - new; + Self::done_rescind(delta); + Imbalance::::new(delta) + } + + /// 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`. + /// + /// This is infallible, but doesn't guarantee that the entire `amount` is issued, for example + /// in the case of overflow. + fn issue(amount: Self::Balance) -> Credit { + let old = Self::total_issuance(); + let new = old.saturating_add(amount); + Self::set_total_issuance(new); + let delta = new - old; + Self::done_issue(delta); + Imbalance::::new(delta) + } + + /// Produce a pair of imbalances that cancel each other out exactly. + /// + /// 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)) + } + + /// 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 + /// 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 + /// added to the account. + fn deposit( + who: &AccountId, + value: Self::Balance, + precision: Precision, + ) -> Result, DispatchError> { + let increase = Self::increase_balance(who, value, precision)?; + Self::done_deposit(who, increase); + Ok(Imbalance::::new(increase)) + } + + /// 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 + /// exactly `value` must be removed from the account of `who` or the operation will fail with an + /// `Err` and nothing will change. + /// + /// If the removal is needed but not possible, then it returns `Err` and nothing is changed. + /// 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 + /// removed from the account. + fn withdraw( + who: &AccountId, + value: Self::Balance, + precision: Precision, + preservation: Preservation, + force: Fortitude, + ) -> Result, DispatchError> { + let decrease = Self::decrease_balance(who, value, precision, preservation, force)?; + Self::done_withdraw(who, decrease); + Ok(Imbalance::::new(decrease)) + } + + /// The balance of `who` is increased in order to counter `credit`. If the whole of `credit` + /// 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 + /// already exist for this to succeed. + fn resolve( + who: &AccountId, + credit: Credit, + ) -> Result<(), Credit> { + let v = credit.peek(); + let debt = match Self::deposit(who, v, Exact) { + Err(_) => return Err(credit), + Ok(d) => d, + }; + let result = credit.offset(debt).try_drop(); + debug_assert!(result.is_ok(), "ok deposit return must be equal to credit value; qed"); + Ok(()) + } + + /// The balance of `who` is decreased in order to counter `debt`. If the whole of `debt` + /// cannot be countered, then nothing is changed and the original `debt` is returned in an + /// `Err`. + fn settle( + who: &AccountId, + debt: Debt, + preservation: Preservation, + ) -> Result, Debt> { + let amount = debt.peek(); + let credit = match Self::withdraw(who, amount, Exact, preservation, Polite) { + Err(_) => return Err(debt), + Ok(d) => d, + }; + + match credit.offset(debt) { + SameOrOther::None => Ok(Credit::::zero()), + SameOrOther::Same(dust) => Ok(dust), + SameOrOther::Other(rest) => { + debug_assert!(false, "ok withdraw return must be at least debt value; qed"); + Err(rest) + }, + } + } + + fn done_rescind(_amount: Self::Balance) {} + fn done_issue(_amount: Self::Balance) {} + fn done_deposit(_who: &AccountId, _amount: Self::Balance) {} + fn done_withdraw(_who: &AccountId, _amount: Self::Balance) {} +} diff --git a/frame/support/src/traits/tokens/fungibles.rs b/frame/support/src/traits/tokens/fungibles.rs deleted file mode 100644 index d146832f3b..0000000000 --- a/frame/support/src/traits/tokens/fungibles.rs +++ /dev/null @@ -1,332 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! The traits for sets of fungible tokens and any associated types. - -use super::{ - misc::{AssetId, Balance}, - *, -}; -use crate::dispatch::{DispatchError, DispatchResult}; -use sp_runtime::traits::Saturating; -use sp_std::vec::Vec; - -pub mod approvals; -mod balanced; -pub mod enumerable; -pub use enumerable::InspectEnumerable; -pub mod metadata; -pub use balanced::{Balanced, Unbalanced}; -mod imbalance; -pub use imbalance::{CreditOf, DebtOf, HandleImbalanceDrop, Imbalance}; -pub mod roles; - -/// Trait for providing balance-inspection access to a set of named fungible assets. -pub trait Inspect { - /// Means of identifying one asset class from another. - type AssetId: AssetId; - - /// Scalar type for representing balance of an account. - type Balance: Balance; - - /// The total amount of issuance in the system. - fn total_issuance(asset: Self::AssetId) -> Self::Balance; - - /// The total amount of issuance in the system excluding those which are controlled by the - /// system. - fn active_issuance(asset: Self::AssetId) -> Self::Balance { - Self::total_issuance(asset) - } - - /// The minimum balance any single account may have. - fn minimum_balance(asset: Self::AssetId) -> Self::Balance; - - /// Get the `asset` balance of `who`. - fn balance(asset: Self::AssetId, who: &AccountId) -> Self::Balance; - - /// Get the maximum amount of `asset` that `who` can withdraw/transfer successfully. - fn reducible_balance(asset: Self::AssetId, who: &AccountId, keep_alive: bool) -> Self::Balance; - - /// Returns `true` if the `asset` balance of `who` may be increased by `amount`. - /// - /// - `asset`: The asset that should be deposited. - /// - `who`: The account of which the balance should be increased by `amount`. - /// - `amount`: How much should the balance be increased? - /// - `mint`: Will `amount` be minted to deposit it into `account`? - fn can_deposit( - asset: Self::AssetId, - who: &AccountId, - amount: Self::Balance, - mint: bool, - ) -> DepositConsequence; - - /// Returns `Failed` if the `asset` balance of `who` may not be decreased by `amount`, otherwise - /// the consequence. - fn can_withdraw( - asset: Self::AssetId, - who: &AccountId, - amount: Self::Balance, - ) -> WithdrawConsequence; - - /// Returns `true` if an `asset` exists. - fn asset_exists(asset: Self::AssetId) -> bool; -} - -/// Trait for reading metadata from a fungible asset. -pub trait InspectMetadata: Inspect { - /// Return the name of an asset. - fn name(asset: &Self::AssetId) -> Vec; - - /// Return the symbol of an asset. - fn symbol(asset: &Self::AssetId) -> Vec; - - /// Return the decimals of an asset. - fn decimals(asset: &Self::AssetId) -> u8; -} - -/// Trait for providing a set of named fungible assets which can be created and destroyed. -pub trait Mutate: Inspect { - /// Attempt to increase the `asset` balance of `who` by `amount`. - /// - /// If not possible then don't do anything. Possible reasons for failure include: - /// - Minimum balance not met. - /// - Account cannot be created (e.g. because there is no provider reference and/or the asset - /// isn't considered worth anything). - /// - /// Since this is an operation which should be possible to take alone, if successful it will - /// increase the overall supply of the underlying token. - fn mint_into(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) -> DispatchResult; - - /// Attempt to reduce the `asset` balance of `who` by `amount`. - /// - /// If not possible then don't do anything. Possible reasons for failure include: - /// - Less funds in the account than `amount` - /// - Liquidity requirements (locks, reservations) prevent the funds from being removed - /// - Operation would require destroying the account and it is required to stay alive (e.g. - /// because it's providing a needed provider reference). - /// - /// Since this is an operation which should be possible to take alone, if successful it will - /// reduce the overall supply of the underlying token. - /// - /// Due to minimum balance requirements, it's possible that the amount withdrawn could be up to - /// `Self::minimum_balance() - 1` more than the `amount`. The total amount withdrawn is returned - /// in an `Ok` result. This may be safely ignored if you don't mind the overall supply reducing. - fn burn_from( - asset: Self::AssetId, - who: &AccountId, - amount: Self::Balance, - ) -> Result; - - /// Attempt to reduce the `asset` balance of `who` by as much as possible up to `amount`, and - /// possibly slightly more due to minimum_balance requirements. If no decrease is possible then - /// an `Err` is returned and nothing is changed. If successful, the amount of tokens reduced is - /// returned. - /// - /// The default implementation just uses `withdraw` along with `reducible_balance` to ensure - /// that is doesn't fail. - fn slash( - asset: Self::AssetId, - who: &AccountId, - amount: Self::Balance, - ) -> Result { - Self::burn_from(asset, who, Self::reducible_balance(asset, who, false).min(amount)) - } - - /// Transfer funds from one account into another. The default implementation uses `mint_into` - /// and `burn_from` and may generate unwanted events. - fn teleport( - asset: Self::AssetId, - source: &AccountId, - dest: &AccountId, - amount: Self::Balance, - ) -> Result { - let extra = Self::can_withdraw(asset, &source, amount).into_result()?; - // As we first burn and then mint, we don't need to check if `mint` fits into the supply. - // If we can withdraw/burn it, we can also mint it again. - Self::can_deposit(asset, dest, amount.saturating_add(extra), false).into_result()?; - let actual = Self::burn_from(asset, source, amount)?; - debug_assert!( - actual == amount.saturating_add(extra), - "can_withdraw must agree with withdraw; qed" - ); - match Self::mint_into(asset, dest, actual) { - Ok(_) => Ok(actual), - Err(err) => { - debug_assert!(false, "can_deposit returned true previously; qed"); - // attempt to return the funds back to source - let revert = Self::mint_into(asset, source, actual); - debug_assert!(revert.is_ok(), "withdrew funds previously; qed"); - Err(err) - }, - } - } -} - -/// Trait for providing a set of named fungible assets which can only be transferred. -pub trait Transfer: Inspect { - /// Transfer funds from one account into another. - fn transfer( - asset: Self::AssetId, - source: &AccountId, - dest: &AccountId, - amount: Self::Balance, - keep_alive: bool, - ) -> Result; - - /// Reduce the active issuance by some amount. - fn deactivate(_: Self::AssetId, _: Self::Balance) {} - - /// Increase the active issuance by some amount, up to the outstanding amount reduced. - fn reactivate(_: Self::AssetId, _: Self::Balance) {} -} - -/// Trait for inspecting a set of named fungible assets which can be placed on hold. -pub trait InspectHold: Inspect { - /// Amount of funds held in hold. - fn balance_on_hold(asset: Self::AssetId, who: &AccountId) -> Self::Balance; - - /// Check to see if some `amount` of `asset` may be held on the account of `who`. - fn can_hold(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) -> bool; -} - -/// Trait for mutating a set of named fungible assets which can be placed on hold. -pub trait MutateHold: InspectHold + Transfer { - /// Hold some funds in an account. - fn hold(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) -> DispatchResult; - - /// Release some funds in an account from being on hold. - /// - /// If `best_effort` is `true`, then the amount actually released and returned as the inner - /// value of `Ok` may be smaller than the `amount` passed. - fn release( - asset: Self::AssetId, - who: &AccountId, - amount: Self::Balance, - best_effort: bool, - ) -> Result; - - /// Transfer held funds into a destination account. - /// - /// If `on_hold` is `true`, then the destination account must already exist and the assets - /// transferred will still be on hold in the destination account. If not, then the destination - /// account need not already exist, but must be creatable. - /// - /// If `best_effort` is `true`, then an amount less than `amount` may be transferred without - /// error. - /// - /// The actual amount transferred is returned, or `Err` in the case of error and nothing is - /// changed. - fn transfer_held( - asset: Self::AssetId, - source: &AccountId, - dest: &AccountId, - amount: Self::Balance, - best_effort: bool, - on_hold: bool, - ) -> Result; -} - -/// Trait for mutating one of several types of fungible assets which can be held. -pub trait BalancedHold: Balanced + MutateHold { - /// Release and slash some funds in an account. - /// - /// The resulting imbalance is the first item of the tuple returned. - /// - /// As much funds up to `amount` will be deducted as possible. If this is less than `amount`, - /// then a non-zero second item will be returned. - fn slash_held( - asset: Self::AssetId, - who: &AccountId, - amount: Self::Balance, - ) -> (CreditOf, Self::Balance); -} - -impl + MutateHold> BalancedHold for T { - fn slash_held( - asset: Self::AssetId, - who: &AccountId, - amount: Self::Balance, - ) -> (CreditOf, Self::Balance) { - let actual = match Self::release(asset, who, amount, true) { - Ok(x) => x, - Err(_) => return (Imbalance::zero(asset), amount), - }; - >::slash(asset, who, actual) - } -} - -/// Trait for providing the ability to create new fungible assets. -pub trait Create: Inspect { - /// Create a new fungible asset. - fn create( - id: Self::AssetId, - admin: AccountId, - is_sufficient: bool, - min_balance: Self::Balance, - ) -> DispatchResult; -} - -/// Trait for providing the ability to destroy existing fungible assets. -pub trait Destroy: Inspect { - /// Start the destruction an existing fungible asset. - /// * `id`: The `AssetId` to be destroyed. successfully. - /// * `maybe_check_owner`: An optional account id that can be used to authorize the destroy - /// command. If not provided, no authorization checks will be performed before destroying - /// asset. - fn start_destroy(id: Self::AssetId, maybe_check_owner: Option) -> DispatchResult; - - /// Destroy all accounts associated with a given asset. - /// `destroy_accounts` should only be called after `start_destroy` has been called, and the - /// asset is in a `Destroying` state - /// - /// * `id`: The identifier of the asset to be destroyed. This must identify an existing asset. - /// * `max_items`: The maximum number of accounts to be destroyed for a given call of the - /// function. This value should be small enough to allow the operation fit into a logical - /// block. - /// - /// Response: - /// * u32: Total number of approvals which were actually destroyed - /// - /// Due to weight restrictions, this function may need to be called multiple - /// times to fully destroy all approvals. It will destroy `max_items` approvals at a - /// time. - fn destroy_accounts(id: Self::AssetId, max_items: u32) -> Result; - /// Destroy all approvals associated with a given asset up to the `max_items` - /// `destroy_approvals` should only be called after `start_destroy` has been called, and the - /// asset is in a `Destroying` state - /// - /// * `id`: The identifier of the asset to be destroyed. This must identify an existing asset. - /// * `max_items`: The maximum number of accounts to be destroyed for a given call of the - /// function. This value should be small enough to allow the operation fit into a logical - /// block. - /// - /// Response: - /// * u32: Total number of approvals which were actually destroyed - /// - /// Due to weight restrictions, this function may need to be called multiple - /// times to fully destroy all approvals. It will destroy `max_items` approvals at a - /// time. - fn destroy_approvals(id: Self::AssetId, max_items: u32) -> Result; - - /// Complete destroying asset and unreserve currency. - /// `finish_destroy` should only be called after `start_destroy` has been called, and the - /// asset is in a `Destroying` state. All accounts or approvals should be destroyed before - /// hand. - /// - /// * `id`: The identifier of the asset to be destroyed. This must identify an existing asset. - fn finish_destroy(id: Self::AssetId) -> DispatchResult; -} diff --git a/frame/support/src/traits/tokens/fungibles/balanced.rs b/frame/support/src/traits/tokens/fungibles/balanced.rs deleted file mode 100644 index 598ba250ee..0000000000 --- a/frame/support/src/traits/tokens/fungibles/balanced.rs +++ /dev/null @@ -1,394 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! The trait and associated types for sets of fungible tokens that manage total issuance without -//! requiring atomic balanced operations. - -use super::*; -use crate::{ - dispatch::{DispatchError, DispatchResult}, - traits::misc::{SameOrOther, TryDrop}, -}; -use sp_arithmetic::traits::Saturating; -use sp_runtime::{ - traits::{CheckedAdd, Zero}, - ArithmeticError, TokenError, -}; -use sp_std::marker::PhantomData; - -/// 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. -pub trait Balanced: Inspect { - /// The type for managing what happens when an instance of `Debt` is dropped without being used. - type OnDropDebt: HandleImbalanceDrop; - /// The type for managing what happens when an instance of `Credit` is dropped without being - /// used. - 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`. - /// - /// This is infallible, but doesn't guarantee that the entire `amount` is burnt, for example - /// in the case of underflow. - fn rescind(asset: Self::AssetId, amount: Self::Balance) -> DebtOf; - - /// 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`. - /// - /// This is infallible, but doesn't guarantee that the entire `amount` is issued, for example - /// in the case of overflow. - fn issue(asset: Self::AssetId, amount: Self::Balance) -> CreditOf; - - /// Produce a pair of imbalances that cancel each other out exactly. - /// - /// This is just the same as burning and issuing the same amount and has no effect on the - /// total issuance. - fn pair( - asset: Self::AssetId, - amount: Self::Balance, - ) -> (DebtOf, CreditOf) { - (Self::rescind(asset, amount), Self::issue(asset, amount)) - } - - /// Deducts up to `value` from the combined balance of `who`, preferring to deduct from the - /// free balance. This function cannot fail. - /// - /// The resulting imbalance is the first item of the tuple returned. - /// - /// As much funds up to `value` will be deducted as possible. If this is less than `value`, - /// then a non-zero second item will be returned. - fn slash( - asset: Self::AssetId, - who: &AccountId, - amount: Self::Balance, - ) -> (CreditOf, Self::Balance); - - /// Mints exactly `value` into the `asset` account of `who`. - /// - /// If `who` doesn't exist, nothing is done and an `Err` returned. This could happen because it - /// the account doesn't yet exist and it isn't possible to create it under the current - /// circumstances and with `value` in it. - fn deposit( - asset: Self::AssetId, - who: &AccountId, - value: Self::Balance, - ) -> Result, DispatchError>; - - /// Removes `value` free `asset` balance from `who` account if possible. - /// - /// If the removal is not possible, then it returns `Err` and nothing is changed. - /// - /// If the operation is successful, this will return `Ok` with a `NegativeImbalance` whose value - /// is no less than `value`. It may be more in the case that removing it reduced it below - /// `Self::minimum_balance()`. - fn withdraw( - asset: Self::AssetId, - who: &AccountId, - value: Self::Balance, - // TODO: liveness: ExistenceRequirement, - ) -> Result, DispatchError>; - - /// The balance of `who` is increased in order to counter `credit`. If the whole of `credit` - /// 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 - /// already exist for this to succeed. - fn resolve( - who: &AccountId, - credit: CreditOf, - ) -> Result<(), CreditOf> { - let v = credit.peek(); - let debt = match Self::deposit(credit.asset(), who, v) { - Err(_) => return Err(credit), - Ok(d) => d, - }; - if let Ok(result) = credit.offset(debt) { - let result = result.try_drop(); - debug_assert!(result.is_ok(), "ok deposit return must be equal to credit value; qed"); - } else { - debug_assert!(false, "debt.asset is credit.asset; qed"); - } - Ok(()) - } - - /// The balance of `who` is decreased in order to counter `debt`. If the whole of `debt` - /// cannot be countered, then nothing is changed and the original `debt` is returned in an - /// `Err`. - fn settle( - who: &AccountId, - debt: DebtOf, - // TODO: liveness: ExistenceRequirement, - ) -> Result, DebtOf> { - let amount = debt.peek(); - let asset = debt.asset(); - let credit = match Self::withdraw(asset, who, amount) { - Err(_) => return Err(debt), - Ok(d) => d, - }; - match credit.offset(debt) { - Ok(SameOrOther::None) => Ok(CreditOf::::zero(asset)), - Ok(SameOrOther::Same(dust)) => Ok(dust), - Ok(SameOrOther::Other(rest)) => { - debug_assert!(false, "ok withdraw return must be at least debt value; qed"); - Err(rest) - }, - Err(_) => { - debug_assert!(false, "debt.asset is credit.asset; qed"); - Ok(CreditOf::::zero(asset)) - }, - } - } -} - -/// A fungible token class where the balance can be set arbitrarily. -/// -/// **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 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 { - /// Set the `asset` balance of `who` to `amount`. If this cannot be done for some reason (e.g. - /// because the account cannot be created or an overflow) then an `Err` is returned. - fn set_balance(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) -> DispatchResult; - - /// Set the total issuance of `asset` to `amount`. - fn set_total_issuance(asset: Self::AssetId, amount: Self::Balance); - - /// Reduce the `asset` balance of `who` by `amount`. If it cannot be reduced by that amount for - /// some reason, return `Err` and don't reduce it at all. If Ok, return the imbalance. - /// - /// Minimum balance will be respected and the returned imbalance may be up to - /// `Self::minimum_balance() - 1` greater than `amount`. - fn decrease_balance( - asset: Self::AssetId, - who: &AccountId, - amount: Self::Balance, - ) -> Result { - let old_balance = Self::balance(asset, who); - let (mut new_balance, mut amount) = if Self::reducible_balance(asset, who, false) < amount { - return Err(TokenError::NoFunds.into()) - } else { - (old_balance - amount, amount) - }; - if new_balance < Self::minimum_balance(asset) { - amount = amount.saturating_add(new_balance); - new_balance = Zero::zero(); - } - // Defensive only - this should not fail now. - Self::set_balance(asset, who, new_balance)?; - Ok(amount) - } - - /// Reduce the `asset` balance of `who` by the most that is possible, up to `amount`. - /// - /// Minimum balance will be respected and the returned imbalance may be up to - /// `Self::minimum_balance() - 1` greater than `amount`. - /// - /// Return the imbalance by which the account was reduced. - fn decrease_balance_at_most( - asset: Self::AssetId, - who: &AccountId, - amount: Self::Balance, - ) -> Self::Balance { - let old_balance = Self::balance(asset, who); - let old_free_balance = Self::reducible_balance(asset, who, false); - let (mut new_balance, mut amount) = if old_free_balance < amount { - (old_balance.saturating_sub(old_free_balance), old_free_balance) - } else { - (old_balance - amount, amount) - }; - let minimum_balance = Self::minimum_balance(asset); - if new_balance < minimum_balance { - amount = amount.saturating_add(new_balance); - new_balance = Zero::zero(); - } - let mut r = Self::set_balance(asset, who, new_balance); - if r.is_err() { - // Some error, probably because we tried to destroy an account which cannot be - // destroyed. - if new_balance.is_zero() && amount >= minimum_balance { - new_balance = minimum_balance; - amount -= minimum_balance; - r = Self::set_balance(asset, who, new_balance); - } - if r.is_err() { - // Still an error. Apparently it's not possible to reduce at all. - amount = Zero::zero(); - } - } - amount - } - - /// Increase the `asset` balance of `who` by `amount`. If it cannot be increased by that amount - /// for some reason, return `Err` and don't increase it at all. If Ok, return the imbalance. - /// - /// Minimum balance will be respected and an error will be returned if - /// `amount < Self::minimum_balance()` when the account of `who` is zero. - fn increase_balance( - asset: Self::AssetId, - who: &AccountId, - amount: Self::Balance, - ) -> Result { - let old_balance = Self::balance(asset, who); - let new_balance = old_balance.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; - if new_balance < Self::minimum_balance(asset) { - return Err(TokenError::BelowMinimum.into()) - } - if old_balance != new_balance { - Self::set_balance(asset, who, new_balance)?; - } - Ok(amount) - } - - /// Increase the `asset` balance of `who` by the most that is possible, up to `amount`. - /// - /// Minimum balance will be respected and the returned imbalance will be zero in the case that - /// `amount < Self::minimum_balance()`. - /// - /// Return the imbalance by which the account was increased. - fn increase_balance_at_most( - asset: Self::AssetId, - who: &AccountId, - amount: Self::Balance, - ) -> Self::Balance { - let old_balance = Self::balance(asset, who); - let mut new_balance = old_balance.saturating_add(amount); - let mut amount = new_balance - old_balance; - if new_balance < Self::minimum_balance(asset) { - new_balance = Zero::zero(); - amount = Zero::zero(); - } - if old_balance == new_balance || Self::set_balance(asset, who, new_balance).is_ok() { - amount - } else { - Zero::zero() - } - } -} - -/// Simple handler for an imbalance drop which increases the total issuance of the system by the -/// imbalance amount. Used for leftover debt. -pub struct IncreaseIssuance(PhantomData<(AccountId, U)>); -impl> HandleImbalanceDrop - for IncreaseIssuance -{ - fn handle(asset: U::AssetId, amount: U::Balance) { - U::set_total_issuance(asset, U::total_issuance(asset).saturating_add(amount)) - } -} - -/// Simple handler for an imbalance drop which decreases the total issuance of the system by the -/// imbalance amount. Used for leftover credit. -pub struct DecreaseIssuance(PhantomData<(AccountId, U)>); -impl> HandleImbalanceDrop - for DecreaseIssuance -{ - fn handle(asset: U::AssetId, amount: U::Balance) { - U::set_total_issuance(asset, U::total_issuance(asset).saturating_sub(amount)) - } -} - -/// An imbalance type which uses `DecreaseIssuance` to deal with anything `Drop`ed. -/// -/// Basically means that funds in someone's account have been removed and not yet placed anywhere -/// else. If it gets dropped, then those funds will be assumed to be "burned" and the total supply -/// will be accordingly decreased to ensure it equals the sum of the balances of all accounts. -type Credit = Imbalance< - >::AssetId, - >::Balance, - DecreaseIssuance, - IncreaseIssuance, ->; - -/// An imbalance type which uses `IncreaseIssuance` to deal with anything `Drop`ed. -/// -/// Basically means that there are funds in someone's account whose origin is as yet unaccounted -/// for. If it gets dropped, then those funds will be assumed to be "minted" and the total supply -/// will be accordingly increased to ensure it equals the sum of the balances of all accounts. -type Debt = Imbalance< - >::AssetId, - >::Balance, - IncreaseIssuance, - DecreaseIssuance, ->; - -/// Create some `Credit` item. Only for internal use. -fn credit>( - asset: U::AssetId, - amount: U::Balance, -) -> Credit { - Imbalance::new(asset, amount) -} - -/// Create some `Debt` item. Only for internal use. -fn debt>( - asset: U::AssetId, - amount: U::Balance, -) -> Debt { - Imbalance::new(asset, amount) -} - -impl> Balanced for U { - type OnDropCredit = DecreaseIssuance; - type OnDropDebt = IncreaseIssuance; - fn rescind(asset: Self::AssetId, amount: Self::Balance) -> Debt { - U::set_total_issuance(asset, U::total_issuance(asset).saturating_sub(amount)); - debt(asset, amount) - } - fn issue(asset: Self::AssetId, amount: Self::Balance) -> Credit { - U::set_total_issuance(asset, U::total_issuance(asset).saturating_add(amount)); - credit(asset, amount) - } - fn slash( - asset: Self::AssetId, - who: &AccountId, - amount: Self::Balance, - ) -> (Credit, Self::Balance) { - let slashed = U::decrease_balance_at_most(asset, who, amount); - // `slashed` could be less than, greater than or equal to `amount`. - // If slashed == amount, it means the account had at least amount in it and it could all be - // removed without a problem. - // If slashed > amount, it means the account had more than amount in it, but not enough more - // to push it over minimum_balance. - // If slashed < amount, it means the account didn't have enough in it to be reduced by - // `amount` without being destroyed. - (credit(asset, slashed), amount.saturating_sub(slashed)) - } - fn deposit( - asset: Self::AssetId, - who: &AccountId, - amount: Self::Balance, - ) -> Result, DispatchError> { - let increase = U::increase_balance(asset, who, amount)?; - Ok(debt(asset, increase)) - } - fn withdraw( - asset: Self::AssetId, - who: &AccountId, - amount: Self::Balance, - // TODO: liveness: ExistenceRequirement, - ) -> Result, DispatchError> { - let decrease = U::decrease_balance(asset, who, amount)?; - Ok(credit(asset, decrease)) - } -} diff --git a/frame/support/src/traits/tokens/fungibles/enumerable.rs b/frame/support/src/traits/tokens/fungibles/enumerable.rs index 5d7266d9f2..08bb784a7d 100644 --- a/frame/support/src/traits/tokens/fungibles/enumerable.rs +++ b/frame/support/src/traits/tokens/fungibles/enumerable.rs @@ -15,10 +15,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::traits::fungibles::Inspect; - /// Interface for enumerating assets in existence or owned by a given account. -pub trait InspectEnumerable: Inspect { +pub trait Inspect: super::Inspect { type AssetsIterator; /// Returns an iterator of the collections in existence. diff --git a/frame/support/src/traits/tokens/fungibles/freeze.rs b/frame/support/src/traits/tokens/fungibles/freeze.rs new file mode 100644 index 0000000000..08549c2d4b --- /dev/null +++ b/frame/support/src/traits/tokens/fungibles/freeze.rs @@ -0,0 +1,78 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! The traits for putting freezes within a single fungible token class. + +use scale_info::TypeInfo; +use sp_runtime::DispatchResult; + +/// Trait for inspecting a fungible asset which can be frozen. Freezing is essentially setting a +/// minimum balance below which the total balance (inclusive of any funds placed on hold) may not +/// be normally allowed to drop. Generally, freezers will provide an "update" function such that +/// if the total balance does drop below the limit, then the freezer can update their housekeeping +/// accordingly. +pub trait Inspect: super::Inspect { + /// An identifier for a freeze. + type Id: codec::Encode + TypeInfo + 'static; + + /// Amount of funds held in reserve by `who` for the given `id`. + fn balance_frozen(asset: Self::AssetId, id: &Self::Id, who: &AccountId) -> Self::Balance; + + /// The amount of the balance which can become frozen. Defaults to `total_balance()`. + fn balance_freezable(asset: Self::AssetId, who: &AccountId) -> Self::Balance { + Self::total_balance(asset, who) + } + + /// Returns `true` if it's possible to introduce a freeze for the given `id` onto the + /// account of `who`. This will be true as long as the implementor supports as many + /// concurrent freeze locks as there are possible values of `id`. + fn can_freeze(asset: Self::AssetId, id: &Self::Id, who: &AccountId) -> bool; +} + +/// Trait for introducing, altering and removing locks to freeze an account's funds so they never +/// go below a set minimum. +pub trait Mutate: Inspect { + /// Prevent actions which would reduce the balance of the account of `who` below the given + /// `amount` and identify this restriction though the given `id`. Unlike `extend_freeze`, any + /// outstanding freeze in place for `who` under the `id` are dropped. + /// + /// If `amount` is zero, it is equivalent to using `thaw`. + /// + /// Note that `amount` can be greater than the total balance, if desired. + fn set_freeze( + asset: Self::AssetId, + id: &Self::Id, + who: &AccountId, + amount: Self::Balance, + ) -> DispatchResult; + + /// Prevent the balance of the account of `who` from being reduced below the given `amount` and + /// identify this restriction though the given `id`. Unlike `set_freeze`, this does not + /// counteract any pre-existing freezes in place for `who` under the `id`. Also unlike + /// `set_freeze`, in the case that `amount` is zero, this is no-op and never fails. + /// + /// Note that more funds can be locked than the total balance, if desired. + fn extend_freeze( + asset: Self::AssetId, + id: &Self::Id, + who: &AccountId, + amount: Self::Balance, + ) -> DispatchResult; + + /// Remove an existing lock. + fn thaw(asset: Self::AssetId, id: &Self::Id, who: &AccountId) -> DispatchResult; +} diff --git a/frame/support/src/traits/tokens/fungibles/hold.rs b/frame/support/src/traits/tokens/fungibles/hold.rs new file mode 100644 index 0000000000..68580ebff4 --- /dev/null +++ b/frame/support/src/traits/tokens/fungibles/hold.rs @@ -0,0 +1,457 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! The traits for putting holds within a single fungible token class. + +use crate::{ + ensure, + traits::tokens::{ + DepositConsequence::Success, + Fortitude::{self, Force}, + Precision::{self, BestEffort, Exact}, + Preservation::{self, Protect}, + Provenance::Extant, + Restriction::{self, Free, OnHold}, + }, +}; +use scale_info::TypeInfo; +use sp_arithmetic::{ + traits::{CheckedAdd, CheckedSub, Zero}, + ArithmeticError, +}; +use sp_runtime::{DispatchError, DispatchResult, Saturating, TokenError}; + +use super::*; + +/// Trait for inspecting a fungible asset whose accounts support partitioning and slashing. +pub trait Inspect: super::Inspect { + /// An identifier for a hold. Used for disambiguating different holds so that + /// they can be individually replaced or removed and funds from one hold don't accidentally + /// become unreserved or slashed for another. + type Reason: codec::Encode + TypeInfo + 'static; + + /// Amount of funds on hold (for all hold reasons) of `who`. + fn total_balance_on_hold(asset: Self::AssetId, who: &AccountId) -> Self::Balance; + + /// Get the maximum amount that the `total_balance_on_hold` of `who` can be reduced successfully + /// based on whether we are willing to force the reduction and potentially go below user-level + /// restrictions on the minimum amount of the account. Note: This cannot bring the account into + /// an inconsistent state with regards any required existential deposit. + /// + /// Always less than `total_balance_on_hold()`. + fn reducible_total_balance_on_hold( + asset: Self::AssetId, + who: &AccountId, + force: Fortitude, + ) -> Self::Balance; + + /// Amount of funds on hold (for the given reason) of `who`. + fn balance_on_hold( + asset: Self::AssetId, + reason: &Self::Reason, + who: &AccountId, + ) -> Self::Balance; + + /// Returns `true` if it's possible to place (additional) funds under a hold of a given + /// `reason`. This may fail if the account has exhausted a limited number of concurrent + /// holds or if it cannot be made to exist (e.g. there is no provider reference). + /// + /// NOTE: This does not take into account changes which could be made to the account of `who` + /// (such as removing a provider reference) after this call is made. Any usage of this should + /// therefore ensure the account is already in the appropriate state prior to calling it. + fn hold_available(asset: Self::AssetId, reason: &Self::Reason, who: &AccountId) -> bool; + + /// Check to see if some `amount` of funds of `who` may be placed on hold with the given + /// `reason`. Reasons why this may not be true: + /// + /// - The implementor supports only a limited number of concurrent holds on an account which is + /// the possible values of `reason`; + /// - The total balance of the account is less than `amount`; + /// - Removing `amount` from the total balance would kill the account and remove the only + /// provider reference. + /// + /// Note: we pass `Fortitude::Force` as the last argument to `reducible_balance` since we assume + /// that if needed the balance can slashed. If we are using a simple non-forcing + /// reserve-transfer, then we really ought to check that we are not reducing the funds below the + /// freeze-limit (if any). + /// + /// NOTE: This does not take into account changes which could be made to the account of `who` + /// (such as removing a provider reference) after this call is made. Any usage of this should + /// therefore ensure the account is already in the appropriate state prior to calling it. + fn ensure_can_hold( + asset: Self::AssetId, + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + ) -> DispatchResult { + ensure!(Self::hold_available(asset, reason, who), TokenError::CannotCreateHold); + ensure!( + amount <= Self::reducible_balance(asset, who, Protect, Force), + TokenError::FundsUnavailable + ); + Ok(()) + } + + /// Check to see if some `amount` of funds of `who` may be placed on hold for the given + /// `reason`. Reasons why this may not be true: + /// + /// - The implementor supports only a limited number of concurrent holds on an account which is + /// the possible values of `reason`; + /// - The main balance of the account is less than `amount`; + /// - Removing `amount` from the main balance would kill the account and remove the only + /// provider reference. + /// + /// NOTE: This does not take into account changes which could be made to the account of `who` + /// (such as removing a provider reference) after this call is made. Any usage of this should + /// therefore ensure the account is already in the appropriate state prior to calling it. + fn can_hold( + asset: Self::AssetId, + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + ) -> bool { + Self::ensure_can_hold(asset, reason, who, amount).is_ok() + } +} + +/// A fungible, holdable token class where the balance on hold can be set arbitrarily. +/// +/// **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 +/// for the underlying datatype to implement so the user gets the much safer `Balanced` trait to +/// use. +pub trait Unbalanced: Inspect { + /// Forcefully set the balance on hold of `who` to `amount`. This is independent of any other + /// balances on hold or the main ("free") balance. + /// + /// If this call executes successfully, you can `assert_eq!(Self::balance_on_hold(), amount);`. + /// + /// This function does its best to force the balance change through, but will not break system + /// invariants such as any Existential Deposits needed or overflows/underflows. + /// If this cannot be done for some reason (e.g. because the account doesn't exist) then an + /// `Err` is returned. + // Implmentation note: This should increment the consumer refs if it moves total on hold from + // zero to non-zero and decrement in the opposite direction. + // + // Since this was not done in the previous logic, this will need either a migration or a + // state item which tracks whether the account is on the old logic or new. + fn set_balance_on_hold( + asset: Self::AssetId, + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + ) -> DispatchResult; + + /// Reduce the balance on hold of `who` by `amount`. + /// + /// If `precision` is `Precision::Exact` and it cannot be reduced by that amount for + /// some reason, return `Err` and don't reduce it at all. If `precision` is + /// `Precision::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. + fn decrease_balance_on_hold( + asset: Self::AssetId, + reason: &Self::Reason, + who: &AccountId, + mut amount: Self::Balance, + precision: Precision, + ) -> Result { + let old_balance = Self::balance_on_hold(asset, reason, who); + if let BestEffort = precision { + amount = amount.min(old_balance); + } + let new_balance = old_balance.checked_sub(&amount).ok_or(TokenError::FundsUnavailable)?; + Self::set_balance_on_hold(asset, reason, who, new_balance)?; + Ok(amount) + } + + /// Increase the balance on hold of `who` by `amount`. + /// + /// If it cannot be increased by that amount for some reason, return `Err` and don't increase + /// it at all. If Ok, return the imbalance. + fn increase_balance_on_hold( + asset: Self::AssetId, + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + let old_balance = Self::balance_on_hold(asset, reason, who); + let new_balance = if let BestEffort = precision { + old_balance.saturating_add(amount) + } else { + old_balance.checked_add(&amount).ok_or(ArithmeticError::Overflow)? + }; + let amount = new_balance.saturating_sub(old_balance); + if !amount.is_zero() { + Self::set_balance_on_hold(asset, reason, who, new_balance)?; + } + Ok(amount) + } +} + +/// Trait for slashing a fungible asset which can be place on hold. +pub trait Balanced: super::Balanced + Unbalanced { + /// Reduce the balance of some funds on hold in an account. + /// + /// The resulting imbalance is the first item of the tuple returned. + /// + /// As much funds that are on hold up to `amount` will be deducted as possible. If this is less + /// than `amount`, then a non-zero second item will be returned. + fn slash( + asset: Self::AssetId, + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + ) -> (Credit, Self::Balance) { + let decrease = Self::decrease_balance_on_hold(asset, reason, who, amount, BestEffort) + .unwrap_or(Default::default()); + let credit = + Imbalance::::new( + asset, decrease, + ); + Self::done_slash(asset, reason, who, decrease); + (credit, amount.saturating_sub(decrease)) + } + + fn done_slash( + _asset: Self::AssetId, + _reason: &Self::Reason, + _who: &AccountId, + _amount: Self::Balance, + ) { + } +} + +/// Trait for mutating a fungible asset which can be placed on hold. +pub trait Mutate: + Inspect + super::Unbalanced + Unbalanced +{ + /// Hold some funds in an account. If a hold for `reason` is already in place, then this + /// will increase it. + fn hold( + asset: Self::AssetId, + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + ) -> DispatchResult { + // NOTE: This doesn't change the total balance of the account so there's no need to + // check liquidity. + + Self::ensure_can_hold(asset, reason, who, amount)?; + // Should be infallible now, but we proceed softly anyway. + Self::decrease_balance(asset, who, amount, Exact, Protect, Force)?; + Self::increase_balance_on_hold(asset, reason, who, amount, BestEffort)?; + Self::done_hold(asset, reason, who, amount); + Ok(()) + } + + /// Release up to `amount` held funds in an account. + /// + /// The actual amount released is returned with `Ok`. + /// + /// If `precision` is `BestEffort`, then the amount actually unreserved and returned as the + /// inner value of `Ok` may be smaller than the `amount` passed. + fn release( + asset: Self::AssetId, + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + // NOTE: This doesn't change the total balance of the account so there's no need to + // check liquidity. + + // We want to make sure we can deposit the amount in advance. If we can't then something is + // very wrong. + ensure!(Self::can_deposit(asset, who, amount, Extant) == Success, TokenError::CannotCreate); + // Get the amount we can actually take from the hold. This might be less than what we want + // if we're only doing a best-effort. + let amount = Self::decrease_balance_on_hold(asset, reason, who, amount, precision)?; + // Increase the main balance by what we took. We always do a best-effort here because we + // already checked that we can deposit before. + let actual = Self::increase_balance(asset, who, amount, BestEffort)?; + Self::done_release(asset, reason, who, actual); + Ok(actual) + } + + /// Attempt to decrease the balance of `who` which is held for the given `reason` by `amount`. + /// + /// If `precision` is true, then as much as possible is reduced, up to `amount`, and the + /// amount of tokens reduced is returned. Otherwise, if the total amount can be reduced, then it + /// is and the amount returned, and if not, then nothing changes and `Err` is returned. + /// + /// If `force` is `Force`, then locks/freezes will be ignored. This should only be used when + /// conducting slashing or other activity which materially disadvantages the account holder + /// since it could provide a means of circumventing freezes. + fn burn_held( + asset: Self::AssetId, + reason: &Self::Reason, + who: &AccountId, + mut amount: Self::Balance, + precision: Precision, + force: Fortitude, + ) -> Result { + // We must check total-balance requirements if `!force`. + let liquid = Self::reducible_total_balance_on_hold(asset, who, force); + if let BestEffort = precision { + amount = amount.min(liquid); + } else { + ensure!(amount <= liquid, TokenError::Frozen); + } + let amount = Self::decrease_balance_on_hold(asset, reason, who, amount, precision)?; + Self::set_total_issuance(asset, Self::total_issuance(asset).saturating_sub(amount)); + Self::done_burn_held(asset, reason, who, amount); + Ok(amount) + } + + /// Transfer held funds into a destination account. + /// + /// If `on_hold` is `true`, then the destination account must already exist and the assets + /// transferred will still be on hold in the destination account. If not, then the destination + /// account need not already exist, but must be creatable. + /// + /// If `precision` is `BestEffort`, then an amount less than `amount` may be transferred without + /// error. + /// + /// If `force` is `Force`, then other fund-locking mechanisms may be disregarded. It should be + /// left as `Regular` in most circumstances, but when you want the same power as a `slash`, it + /// may be `Force`. + /// + /// The actual amount transferred is returned, or `Err` in the case of error and nothing is + /// changed. + fn transfer_on_hold( + asset: Self::AssetId, + reason: &Self::Reason, + source: &AccountId, + dest: &AccountId, + mut amount: Self::Balance, + precision: Precision, + mode: Restriction, + force: Fortitude, + ) -> Result { + // We must check total-balance requirements if `!force`. + let have = Self::balance_on_hold(asset, reason, source); + let liquid = Self::reducible_total_balance_on_hold(asset, source, force); + if let BestEffort = precision { + amount = amount.min(liquid).min(have); + } else { + ensure!(amount <= liquid, TokenError::Frozen); + ensure!(amount <= have, TokenError::FundsUnavailable); + } + + // We want to make sure we can deposit the amount in advance. If we can't then something is + // very wrong. + ensure!( + Self::can_deposit(asset, dest, amount, Extant) == Success, + TokenError::CannotCreate + ); + ensure!( + mode == Free || Self::hold_available(asset, reason, dest), + TokenError::CannotCreateHold + ); + + let amount = Self::decrease_balance_on_hold(asset, reason, source, amount, precision)?; + let actual = if mode == OnHold { + Self::increase_balance_on_hold(asset, reason, dest, amount, precision)? + } else { + Self::increase_balance(asset, dest, amount, precision)? + }; + Self::done_transfer_on_hold(asset, reason, source, dest, actual); + Ok(actual) + } + + /// Transfer some `amount` of free balance from `source` to become owned by `dest` but on hold + /// for `reason`. + /// for `reason`. + /// + /// If `precision` is `BestEffort`, then an amount less than `amount` may be transferred without + /// error. + /// + /// `source` must obey the requirements of `keep_alive`. + /// + /// If `force` is `Force`, then other fund-locking mechanisms may be disregarded. It should be + /// left as `Regular` in most circumstances, but when you want the same power as a `slash`, it + /// may be `Force`. + /// + /// The amount placed on hold is returned or `Err` in the case of error and nothing is changed. + /// + /// WARNING: This may return an error after a partial storage mutation. It should be used only + /// inside a transactional storage context and an `Err` result must imply a storage rollback. + fn transfer_and_hold( + asset: Self::AssetId, + reason: &Self::Reason, + source: &AccountId, + dest: &AccountId, + amount: Self::Balance, + precision: Precision, + expendability: Preservation, + force: Fortitude, + ) -> Result { + ensure!(Self::hold_available(asset, reason, dest), TokenError::CannotCreateHold); + ensure!( + Self::can_deposit(asset, dest, amount, Extant) == Success, + TokenError::CannotCreate + ); + let actual = + Self::decrease_balance(asset, source, amount, precision, expendability, force)?; + Self::increase_balance_on_hold(asset, reason, dest, actual, precision)?; + Self::done_transfer_on_hold(asset, reason, source, dest, actual); + Ok(actual) + } + + fn done_hold( + _asset: Self::AssetId, + _reason: &Self::Reason, + _who: &AccountId, + _amount: Self::Balance, + ) { + } + fn done_release( + _asset: Self::AssetId, + _reason: &Self::Reason, + _who: &AccountId, + _amount: Self::Balance, + ) { + } + fn done_burn_held( + _asset: Self::AssetId, + _reason: &Self::Reason, + _who: &AccountId, + _amount: Self::Balance, + ) { + } + fn done_transfer_on_hold( + _asset: Self::AssetId, + _reason: &Self::Reason, + _source: &AccountId, + _dest: &AccountId, + _amount: Self::Balance, + ) { + } + fn done_transfer_and_hold( + _asset: Self::AssetId, + _reason: &Self::Reason, + _source: &AccountId, + _dest: &AccountId, + _transferred: Self::Balance, + ) { + } +} diff --git a/frame/support/src/traits/tokens/fungibles/imbalance.rs b/frame/support/src/traits/tokens/fungibles/imbalance.rs index 87445859cb..ab18eec381 100644 --- a/frame/support/src/traits/tokens/fungibles/imbalance.rs +++ b/frame/support/src/traits/tokens/fungibles/imbalance.rs @@ -18,12 +18,11 @@ //! The imbalance type and its associates, which handles keeps everything adding up properly with //! unbalanced operations. -use super::{ - balanced::Balanced, - fungibles::{AssetId, Balance}, - *, +use super::*; +use crate::traits::{ + misc::{SameOrOther, TryDrop}, + tokens::{AssetId, Balance}, }; -use crate::traits::misc::{SameOrOther, TryDrop}; use sp_runtime::{traits::Zero, RuntimeDebug}; use sp_std::marker::PhantomData; @@ -160,7 +159,7 @@ impl< } /// Imbalance implying that the total_issuance value is less than the sum of all account balances. -pub type DebtOf = Imbalance< +pub type Debt = Imbalance< >::AssetId, >::Balance, // This will generally be implemented by increasing the total_issuance value. @@ -170,7 +169,7 @@ pub type DebtOf = Imbalance< /// Imbalance implying that the total_issuance value is greater than the sum of all account /// balances. -pub type CreditOf = Imbalance< +pub type Credit = Imbalance< >::AssetId, >::Balance, // This will generally be implemented by decreasing the total_issuance value. diff --git a/frame/support/src/traits/tokens/fungibles/lifetime.rs b/frame/support/src/traits/tokens/fungibles/lifetime.rs new file mode 100644 index 0000000000..9e2c306f6f --- /dev/null +++ b/frame/support/src/traits/tokens/fungibles/lifetime.rs @@ -0,0 +1,84 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Traits for creating and destroying assets. + +use sp_runtime::{DispatchError, DispatchResult}; + +use super::Inspect; + +/// Trait for providing the ability to create new fungible assets. +pub trait Create: Inspect { + /// Create a new fungible asset. + fn create( + id: Self::AssetId, + admin: AccountId, + is_sufficient: bool, + min_balance: Self::Balance, + ) -> DispatchResult; +} + +/// Trait for providing the ability to destroy existing fungible assets. +pub trait Destroy: Inspect { + /// Start the destruction an existing fungible asset. + /// * `id`: The `AssetId` to be destroyed. successfully. + /// * `maybe_check_owner`: An optional account id that can be used to authorize the destroy + /// command. If not provided, no authorization checks will be performed before destroying + /// asset. + fn start_destroy(id: Self::AssetId, maybe_check_owner: Option) -> DispatchResult; + + /// Destroy all accounts associated with a given asset. + /// `destroy_accounts` should only be called after `start_destroy` has been called, and the + /// asset is in a `Destroying` state + /// + /// * `id`: The identifier of the asset to be destroyed. This must identify an existing asset. + /// * `max_items`: The maximum number of accounts to be destroyed for a given call of the + /// function. This value should be small enough to allow the operation fit into a logical + /// block. + /// + /// Response: + /// * u32: Total number of approvals which were actually destroyed + /// + /// Due to weight restrictions, this function may need to be called multiple + /// times to fully destroy all approvals. It will destroy `max_items` approvals at a + /// time. + fn destroy_accounts(id: Self::AssetId, max_items: u32) -> Result; + /// Destroy all approvals associated with a given asset up to the `max_items` + /// `destroy_approvals` should only be called after `start_destroy` has been called, and the + /// asset is in a `Destroying` state + /// + /// * `id`: The identifier of the asset to be destroyed. This must identify an existing asset. + /// * `max_items`: The maximum number of accounts to be destroyed for a given call of the + /// function. This value should be small enough to allow the operation fit into a logical + /// block. + /// + /// Response: + /// * u32: Total number of approvals which were actually destroyed + /// + /// Due to weight restrictions, this function may need to be called multiple + /// times to fully destroy all approvals. It will destroy `max_items` approvals at a + /// time. + fn destroy_approvals(id: Self::AssetId, max_items: u32) -> Result; + + /// Complete destroying asset and unreserve currency. + /// `finish_destroy` should only be called after `start_destroy` has been called, and the + /// asset is in a `Destroying` state. All accounts or approvals should be destroyed before + /// hand. + /// + /// * `id`: The identifier of the asset to be destroyed. This must identify an existing asset. + fn finish_destroy(id: Self::AssetId) -> DispatchResult; +} diff --git a/frame/support/src/traits/tokens/fungibles/mod.rs b/frame/support/src/traits/tokens/fungibles/mod.rs new file mode 100644 index 0000000000..697eff39ff --- /dev/null +++ b/frame/support/src/traits/tokens/fungibles/mod.rs @@ -0,0 +1,40 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! The traits for sets of fungible tokens and any associated types. + +pub mod approvals; +mod enumerable; +pub mod freeze; +pub mod hold; +mod imbalance; +mod lifetime; +pub mod metadata; +mod regular; +pub mod roles; + +pub use enumerable::Inspect as InspectEnumerable; +pub use freeze::{Inspect as InspectFreeze, Mutate as MutateFreeze}; +pub use hold::{ + Balanced as BalancedHold, Inspect as InspectHold, Mutate as MutateHold, + Unbalanced as UnbalancedHold, +}; +pub use imbalance::{Credit, Debt, HandleImbalanceDrop, Imbalance}; +pub use lifetime::{Create, Destroy}; +pub use regular::{ + Balanced, DecreaseIssuance, Dust, IncreaseIssuance, Inspect, Mutate, Unbalanced, +}; diff --git a/frame/support/src/traits/tokens/fungibles/regular.rs b/frame/support/src/traits/tokens/fungibles/regular.rs new file mode 100644 index 0000000000..03c1498801 --- /dev/null +++ b/frame/support/src/traits/tokens/fungibles/regular.rs @@ -0,0 +1,571 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! `Inspect` and `Mutate` traits for working with regular balances. + +use sp_std::marker::PhantomData; + +use crate::{ + dispatch::DispatchError, + ensure, + traits::{ + tokens::{ + misc::{ + Balance, DepositConsequence, + Fortitude::{self, Force, Polite}, + Precision::{self, BestEffort, Exact}, + Preservation::{self, Expendable}, + Provenance::{self, Extant}, + WithdrawConsequence, + }, + AssetId, + }, + SameOrOther, TryDrop, + }, +}; +use sp_arithmetic::traits::{CheckedAdd, CheckedSub, One}; +use sp_runtime::{traits::Saturating, ArithmeticError, TokenError}; + +use super::{Credit, Debt, HandleImbalanceDrop, Imbalance}; + +/// Trait for providing balance-inspection access to a set of named fungible assets. +pub trait Inspect: Sized { + /// Means of identifying one asset class from another. + type AssetId: AssetId; + + /// Scalar type for representing balance of an account. + type Balance: Balance; + + /// The total amount of issuance in the system. + fn total_issuance(asset: Self::AssetId) -> Self::Balance; + + /// The total amount of issuance in the system excluding those which are controlled by the + /// system. + fn active_issuance(asset: Self::AssetId) -> Self::Balance { + Self::total_issuance(asset) + } + + /// The minimum balance any single account may have. + fn minimum_balance(asset: Self::AssetId) -> Self::Balance; + + /// Get the total amount of funds whose ultimate bneficial ownership can be determined as `who`. + /// + /// This may include funds which are wholly inaccessible to `who`, either temporarily or even + /// indefinitely. + /// + /// For the amount of the balance which is currently free to be removed from the account without + /// error, use `reducible_balance`. + /// + /// For the amount of the balance which may eventually be free to be removed from the account, + /// use `balance()`. + fn total_balance(asset: Self::AssetId, who: &AccountId) -> Self::Balance; + + /// Get the balance of `who` which does not include funds which are exclusively allocated to + /// 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()`. + fn balance(asset: Self::AssetId, who: &AccountId) -> Self::Balance; + + /// Get the maximum amount that `who` can withdraw/transfer successfully based on whether the + /// account should be kept alive (`preservation`) or whether we are willing to force the + /// transfer and potentially go below user-level restrictions on the minimum amount of the + /// account. + /// + /// Always less than `free_balance()`. + fn reducible_balance( + asset: Self::AssetId, + who: &AccountId, + preservation: Preservation, + force: Fortitude, + ) -> Self::Balance; + + /// Returns `true` if the `asset` balance of `who` may be increased by `amount`. + /// + /// - `asset`: The asset that should be deposited. + /// - `who`: The account of which the balance should be increased by `amount`. + /// - `amount`: How much should the balance be increased? + /// - `mint`: Will `amount` be minted to deposit it into `account`? + fn can_deposit( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + provenance: Provenance, + ) -> DepositConsequence; + + /// Returns `Failed` if the `asset` balance of `who` may not be decreased by `amount`, otherwise + /// the consequence. + fn can_withdraw( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + ) -> WithdrawConsequence; + + /// Returns `true` if an `asset` exists. + fn asset_exists(asset: Self::AssetId) -> bool; +} + +/// Special dust type which can be type-safely converted into a `Credit`. +#[must_use] +pub struct Dust>(pub(crate) T::AssetId, pub(crate) T::Balance); + +impl> Dust { + /// Convert `Dust` into an instance of `Credit`. + pub fn into_credit(self) -> Credit { + Credit::::new(self.0, self.1) + } +} + +/// A fungible token class where the balance can be set arbitrarily. +/// +/// **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 +/// 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`. + /// + /// This should not be reimplemented. + fn handle_raw_dust(asset: Self::AssetId, amount: Self::Balance) { + Self::handle_dust(Dust( + asset, + amount.min(Self::minimum_balance(asset).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. + fn handle_dust(dust: Dust); + + /// Forcefully set the balance of `who` to `amount`. + /// + /// If this call executes successfully, you can `assert_eq!(Self::balance(), amount);`. + /// + /// For implementations which include one or more balances on hold, then these are *not* + /// included in the `amount`. + /// + /// This function does its best to force the balance change through, but will not break system + /// invariants such as any Existential Deposits needed or overflows/underflows. + /// If this cannot be done for some reason (e.g. because the account cannot be created, deleted + /// or would overflow) then an `Err` is returned. + fn write_balance( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + ) -> Result, DispatchError>; + + /// Set the total issuance to `amount`. + fn set_total_issuance(asset: Self::AssetId, amount: Self::Balance); + + /// 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 + /// 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. + fn decrease_balance( + asset: Self::AssetId, + who: &AccountId, + mut amount: Self::Balance, + precision: Precision, + preservation: Preservation, + force: Fortitude, + ) -> Result { + let old_balance = Self::balance(asset, who); + let free = Self::reducible_balance(asset, who, preservation, force); + if let BestEffort = precision { + amount = amount.min(free); + } + let new_balance = old_balance.checked_sub(&amount).ok_or(TokenError::FundsUnavailable)?; + if let Some(dust) = Self::write_balance(asset, who, new_balance)? { + Self::handle_dust(Dust(asset, dust)); + } + Ok(old_balance.saturating_sub(new_balance)) + } + + /// Increase the balance of `who` by `amount`. + /// + /// If it cannot be increased by that amount for some reason, return `Err` and don't increase + /// it at all. If Ok, return the imbalance. + /// Minimum balance will be respected and an error will be returned if + /// `amount < Self::minimum_balance()` when the account of `who` is zero. + fn increase_balance( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + let old_balance = Self::balance(asset, who); + let new_balance = if let BestEffort = precision { + old_balance.saturating_add(amount) + } else { + old_balance.checked_add(&amount).ok_or(ArithmeticError::Overflow)? + }; + if new_balance < Self::minimum_balance(asset) { + // Attempt to increase from 0 to below minimum -> stays at zero. + if let BestEffort = precision { + Ok(Self::Balance::default()) + } else { + Err(TokenError::BelowMinimum.into()) + } + } else { + if new_balance == old_balance { + Ok(Self::Balance::default()) + } else { + if let Some(dust) = Self::write_balance(asset, who, new_balance)? { + Self::handle_dust(Dust(asset, dust)); + } + Ok(new_balance.saturating_sub(old_balance)) + } + } + } + + /// Reduce the active issuance by some amount. + fn deactivate(_asset: Self::AssetId, _: Self::Balance) {} + + /// Increase the active issuance by some amount, up to the outstanding amount reduced. + fn reactivate(_asset: Self::AssetId, _: Self::Balance) {} +} + +/// Trait for providing a basic fungible asset. +pub trait Mutate: Inspect + Unbalanced { + /// Increase the balance of `who` by exactly `amount`, minting new tokens. If that isn't + /// possible then an `Err` is returned and nothing is changed. + fn mint_into( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + ) -> Result { + Self::total_issuance(asset) + .checked_add(&amount) + .ok_or(ArithmeticError::Overflow)?; + let actual = Self::increase_balance(asset, who, amount, Exact)?; + Self::set_total_issuance(asset, Self::total_issuance(asset).saturating_add(actual)); + Self::done_mint_into(asset, who, amount); + Ok(actual) + } + + /// Decrease the balance of `who` by at least `amount`, possibly slightly more in the case of + /// minimum-balance requirements, burning the tokens. If that isn't possible then an `Err` is + /// returned and nothing is changed. If successful, the amount of tokens reduced is returned. + fn burn_from( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + precision: Precision, + force: Fortitude, + ) -> Result { + let actual = Self::reducible_balance(asset, who, Expendable, force).min(amount); + ensure!(actual == amount || precision == BestEffort, TokenError::FundsUnavailable); + Self::total_issuance(asset) + .checked_sub(&actual) + .ok_or(ArithmeticError::Overflow)?; + let actual = Self::decrease_balance(asset, who, actual, BestEffort, Expendable, force)?; + Self::set_total_issuance(asset, Self::total_issuance(asset).saturating_sub(actual)); + Self::done_burn_from(asset, who, actual); + Ok(actual) + } + + /// 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 + /// 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). + /// + /// Because of this expectation, any metadata associated with the asset is expected to survive + /// the suspect-resume cycle. + fn shelve( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + ) -> Result { + let actual = Self::reducible_balance(asset, who, Expendable, Polite).min(amount); + ensure!(actual == amount, TokenError::FundsUnavailable); + Self::total_issuance(asset) + .checked_sub(&actual) + .ok_or(ArithmeticError::Overflow)?; + let actual = Self::decrease_balance(asset, who, actual, BestEffort, Expendable, Polite)?; + Self::set_total_issuance(asset, Self::total_issuance(asset).saturating_sub(actual)); + Self::done_shelve(asset, who, actual); + Ok(actual) + } + + /// 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 + /// 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). + /// + /// Because of this expectation, any metadata associated with the asset is expected to survive + /// the suspect-resume cycle. + fn restore( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + ) -> Result { + Self::total_issuance(asset) + .checked_add(&amount) + .ok_or(ArithmeticError::Overflow)?; + let actual = Self::increase_balance(asset, who, amount, Exact)?; + Self::set_total_issuance(asset, Self::total_issuance(asset).saturating_add(actual)); + Self::done_restore(asset, who, amount); + Ok(actual) + } + + /// Transfer funds from one account into another. + fn transfer( + asset: Self::AssetId, + source: &AccountId, + dest: &AccountId, + amount: Self::Balance, + preservation: Preservation, + ) -> Result { + let _extra = + Self::can_withdraw(asset, source, amount).into_result(preservation != Expendable)?; + Self::can_deposit(asset, dest, amount, Extant).into_result()?; + Self::decrease_balance(asset, source, amount, BestEffort, preservation, Polite)?; + // This should never fail as we checked `can_deposit` earlier. But we do a best-effort + // anyway. + let _ = Self::increase_balance(asset, dest, amount, BestEffort); + Self::done_transfer(asset, source, dest, amount); + Ok(amount) + } + + /// Simple infallible function to force an account to have a particular balance, good for use + /// in tests and benchmarks but not recommended for production code owing to the lack of + /// error reporting. + /// + /// Returns the new balance. + fn set_balance(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) -> Self::Balance { + let b = Self::balance(asset, who); + if b > amount { + Self::burn_from(asset, who, b - amount, BestEffort, Force) + .map(|d| amount.saturating_sub(d)) + } else { + Self::mint_into(asset, who, amount - b).map(|d| amount.saturating_add(d)) + } + .unwrap_or(b) + } + fn done_mint_into(_asset: Self::AssetId, _who: &AccountId, _amount: Self::Balance) {} + fn done_burn_from(_asset: Self::AssetId, _who: &AccountId, _amount: Self::Balance) {} + fn done_shelve(_asset: Self::AssetId, _who: &AccountId, _amount: Self::Balance) {} + fn done_restore(_asset: Self::AssetId, _who: &AccountId, _amount: Self::Balance) {} + fn done_transfer( + _asset: Self::AssetId, + _source: &AccountId, + _dest: &AccountId, + _amount: Self::Balance, + ) { + } +} + +/// Simple handler for an imbalance drop which increases the total issuance of the system by the +/// imbalance amount. Used for leftover debt. +pub struct IncreaseIssuance(PhantomData<(AccountId, U)>); +impl> HandleImbalanceDrop + for IncreaseIssuance +{ + fn handle(asset: U::AssetId, amount: U::Balance) { + U::set_total_issuance(asset, U::total_issuance(asset).saturating_add(amount)) + } +} + +/// Simple handler for an imbalance drop which decreases the total issuance of the system by the +/// imbalance amount. Used for leftover credit. +pub struct DecreaseIssuance(PhantomData<(AccountId, U)>); +impl> HandleImbalanceDrop + for DecreaseIssuance +{ + fn handle(asset: U::AssetId, amount: U::Balance) { + U::set_total_issuance(asset, U::total_issuance(asset).saturating_sub(amount)) + } +} + +/// 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. +pub trait Balanced: Inspect + Unbalanced { + /// The type for managing what happens when an instance of `Debt` is dropped without being used. + type OnDropDebt: HandleImbalanceDrop; + /// The type for managing what happens when an instance of `Credit` is dropped without being + /// used. + 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`. + /// + /// This is infallible, but doesn't guarantee that the entire `amount` is burnt, for example + /// in the case of underflow. + fn rescind(asset: Self::AssetId, amount: Self::Balance) -> Debt { + let old = Self::total_issuance(asset); + let new = old.saturating_sub(amount); + Self::set_total_issuance(asset, new); + let delta = old - new; + Self::done_rescind(asset, delta); + Imbalance::::new( + asset, delta, + ) + } + + /// 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`. + /// + /// This is infallible, but doesn't guarantee that the entire `amount` is issued, for example + /// in the case of overflow. + fn issue(asset: Self::AssetId, amount: Self::Balance) -> Credit { + let old = Self::total_issuance(asset); + let new = old.saturating_add(amount); + Self::set_total_issuance(asset, new); + let delta = new - old; + Self::done_issue(asset, delta); + Imbalance::::new( + asset, delta, + ) + } + + /// Produce a pair of imbalances that cancel each other out exactly. + /// + /// This is just the same as burning and issuing the same amount and has no effect on the + /// total issuance. + fn pair( + asset: Self::AssetId, + amount: Self::Balance, + ) -> (Debt, Credit) { + (Self::rescind(asset, amount), Self::issue(asset, amount)) + } + + /// Mints `value` into the account of `who`, creating it as needed. + /// + /// If `precision` is `BestEffort` and `value` in full could not be minted (e.g. due to + /// overflow), then the maximum is minted, up to `value`. If `precision` is `Exact`, then + /// 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 + /// added to the account. + fn deposit( + asset: Self::AssetId, + who: &AccountId, + value: Self::Balance, + precision: Precision, + ) -> Result, DispatchError> { + let increase = Self::increase_balance(asset, who, value, precision)?; + Self::done_deposit(asset, who, increase); + Ok(Imbalance::::new( + asset, increase, + )) + } + + /// 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 + /// exactly `value` must be removed from the account of `who` or the operation will fail with an + /// `Err` and nothing will change. + /// + /// If the removal is needed but not possible, then it returns `Err` and nothing is changed. + /// 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 + /// removed from the account. + fn withdraw( + asset: Self::AssetId, + who: &AccountId, + value: Self::Balance, + precision: Precision, + preservation: Preservation, + force: Fortitude, + ) -> Result, DispatchError> { + let decrease = Self::decrease_balance(asset, who, value, precision, preservation, force)?; + Self::done_withdraw(asset, who, decrease); + Ok(Imbalance::::new( + asset, decrease, + )) + } + + /// The balance of `who` is increased in order to counter `credit`. If the whole of `credit` + /// 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 + /// already exist for this to succeed. + fn resolve( + who: &AccountId, + credit: Credit, + ) -> Result<(), Credit> { + let v = credit.peek(); + let debt = match Self::deposit(credit.asset(), who, v, Exact) { + Err(_) => return Err(credit), + Ok(d) => d, + }; + if let Ok(result) = credit.offset(debt) { + let result = result.try_drop(); + debug_assert!(result.is_ok(), "ok deposit return must be equal to credit value; qed"); + } else { + debug_assert!(false, "debt.asset is credit.asset; qed"); + } + Ok(()) + } + + /// The balance of `who` is decreased in order to counter `debt`. If the whole of `debt` + /// cannot be countered, then nothing is changed and the original `debt` is returned in an + /// `Err`. + fn settle( + who: &AccountId, + debt: Debt, + preservation: Preservation, + ) -> Result, Debt> { + let amount = debt.peek(); + let asset = debt.asset(); + let credit = match Self::withdraw(asset, who, amount, Exact, preservation, Polite) { + Err(_) => return Err(debt), + Ok(d) => d, + }; + match credit.offset(debt) { + Ok(SameOrOther::None) => Ok(Credit::::zero(asset)), + Ok(SameOrOther::Same(dust)) => Ok(dust), + Ok(SameOrOther::Other(rest)) => { + debug_assert!(false, "ok withdraw return must be at least debt value; qed"); + Err(rest) + }, + Err(_) => { + debug_assert!(false, "debt.asset is credit.asset; qed"); + Ok(Credit::::zero(asset)) + }, + } + } + + fn done_rescind(_asset: Self::AssetId, _amount: Self::Balance) {} + fn done_issue(_asset: Self::AssetId, _amount: Self::Balance) {} + fn done_deposit(_asset: Self::AssetId, _who: &AccountId, _amount: Self::Balance) {} + fn done_withdraw(_asset: Self::AssetId, _who: &AccountId, _amount: Self::Balance) {} +} diff --git a/frame/support/src/traits/tokens/misc.rs b/frame/support/src/traits/tokens/misc.rs index 6113642c83..8ad3a9535f 100644 --- a/frame/support/src/traits/tokens/misc.rs +++ b/frame/support/src/traits/tokens/misc.rs @@ -23,12 +23,65 @@ use sp_core::RuntimeDebug; use sp_runtime::{traits::Convert, ArithmeticError, DispatchError, TokenError}; use sp_std::fmt::Debug; +/// The origin of funds to be used for a deposit operation. +#[derive(Copy, Clone, RuntimeDebug, Eq, PartialEq)] +pub enum Provenance { + /// The funds will be minted into the system, increasing total issuance (and potentially + /// causing an overflow there). + Minted, + /// The funds already exist in the system, therefore will not affect total issuance. + Extant, +} + +/// The mode under which usage of funds may be restricted. +#[derive(Copy, Clone, RuntimeDebug, Eq, PartialEq)] +pub enum Restriction { + /// Funds are under the normal conditions. + Free, + /// Funds are on hold. + OnHold, +} + +/// The mode by which we describe whether an operation should keep an account alive. +#[derive(Copy, Clone, RuntimeDebug, Eq, PartialEq)] +pub enum Preservation { + /// We don't care if the account gets killed by this operation. + Expendable, + /// The account may not be killed, but we don't care if the balance gets dusted. + Protect, + /// 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, +} + +/// The privilege with which a withdraw operation is conducted. +#[derive(Copy, Clone, RuntimeDebug, Eq, PartialEq)] +pub enum Fortitude { + /// The operation should execute with regular privilege. + Polite, + /// The operation should be forced to succeed if possible. This is usually employed for system- + /// level security-critical events such as slashing. + Force, +} + +/// The precision required of an operation generally involving some aspect of quantitative fund +/// withdrawal or transfer. +#[derive(Copy, Clone, RuntimeDebug, Eq, PartialEq)] +pub enum Precision { + /// The operation should must either proceed either exactly according to the amounts involved + /// or not at all. + Exact, + /// The operation may be considered successful even if less than the specified amounts are + /// available to be used. In this case a best effort will be made. + BestEffort, +} + /// One of a number of consequences of withdrawing a fungible from an account. #[derive(Copy, Clone, RuntimeDebug, Eq, PartialEq)] pub enum WithdrawConsequence { /// Withdraw could not happen since the amount to be withdrawn is less than the total funds in /// the account. - NoFunds, + BalanceLow, /// The withdraw would mean the account dying when it needs to exist (usually because it is a /// provider and there are consumer references on it). WouldDie, @@ -53,15 +106,16 @@ pub enum WithdrawConsequence { impl WithdrawConsequence { /// Convert the type into a `Result` with `DispatchError` as the error or the additional /// `Balance` by which the account will be reduced. - pub fn into_result(self) -> Result { + pub fn into_result(self, keep_nonzero: bool) -> Result { use WithdrawConsequence::*; match self { - NoFunds => Err(TokenError::NoFunds.into()), - WouldDie => Err(TokenError::WouldDie.into()), + BalanceLow => Err(TokenError::FundsUnavailable.into()), + WouldDie => Err(TokenError::OnlyProvider.into()), UnknownAsset => Err(TokenError::UnknownAsset.into()), Underflow => Err(ArithmeticError::Underflow.into()), Overflow => Err(ArithmeticError::Overflow.into()), Frozen => Err(TokenError::Frozen.into()), + ReducedToZero(_) if keep_nonzero => Err(TokenError::NotExpendable.into()), ReducedToZero(result) => Ok(result), Success => Ok(Zero::zero()), } diff --git a/frame/support/src/traits/tokens/pay.rs b/frame/support/src/traits/tokens/pay.rs index 1c6a147b5f..23bd113bfe 100644 --- a/frame/support/src/traits/tokens/pay.rs +++ b/frame/support/src/traits/tokens/pay.rs @@ -22,7 +22,7 @@ use scale_info::TypeInfo; use sp_core::{RuntimeDebug, TypedGet}; use sp_std::fmt::Debug; -use super::{fungible, Balance}; +use super::{fungible, Balance, Preservation::Expendable}; /// Can be implemented by `PayFromAccount` using a `fungible` impl, but can also be implemented with /// XCM/MultiAsset and made generic over assets. @@ -76,9 +76,7 @@ pub enum PaymentStatus { /// Simple implementation of `Pay` which makes a payment from a "pot" - i.e. a single account. pub struct PayFromAccount(sp_std::marker::PhantomData<(F, A)>); -impl + fungible::Mutate> Pay - for PayFromAccount -{ +impl> Pay for PayFromAccount { type Balance = F::Balance; type Beneficiary = A::Type; type AssetKind = (); @@ -88,7 +86,7 @@ impl + fungible::Mutate> Pa _: Self::AssetKind, amount: Self::Balance, ) -> Result { - >::transfer(&A::get(), who, amount, false).map_err(|_| ())?; + >::transfer(&A::get(), who, amount, Expendable).map_err(|_| ())?; Ok(()) } fn check_payment(_: ()) -> PaymentStatus { diff --git a/frame/system/src/lib.rs b/frame/system/src/lib.rs index faa1ee5d6c..88291c326e 100644 --- a/frame/system/src/lib.rs +++ b/frame/system/src/lib.rs @@ -1661,10 +1661,6 @@ impl BlockNumberProvider for Pallet { } } -fn is_providing(d: &T) -> bool { - d != &T::default() -} - /// Implement StoredMap for a simple single-item, provide-when-not-default system. This works fine /// for storing a single item which allows the account to continue existing as long as it's not /// empty/default. @@ -1680,23 +1676,12 @@ impl StoredMap for Pallet { f: impl FnOnce(&mut Option) -> Result, ) -> Result { let account = Account::::get(k); - let was_providing = is_providing(&account.data); - let mut some_data = if was_providing { Some(account.data) } else { None }; + let is_default = account.data == T::AccountData::default(); + let mut some_data = if is_default { None } else { Some(account.data) }; let result = f(&mut some_data)?; - let is_providing = some_data.is_some(); - if !was_providing && is_providing { - Self::inc_providers(k); - } else if was_providing && !is_providing { - match Self::dec_providers(k)? { - DecRefStatus::Reaped => return Ok(result), - DecRefStatus::Exists => { - // Update value as normal... - }, - } - } else if !was_providing && !is_providing { - return Ok(result) + if Self::providers(k) > 0 { + Account::::mutate(k, |a| a.data = some_data.unwrap_or_default()); } - Account::::mutate(k, |a| a.data = some_data.unwrap_or_default()); Ok(result) } } diff --git a/frame/system/src/tests.rs b/frame/system/src/tests.rs index ebb28ca87b..9b60c6915a 100644 --- a/frame/system/src/tests.rs +++ b/frame/system/src/tests.rs @@ -37,6 +37,7 @@ fn origin_works() { #[test] fn stored_map_works() { new_test_ext().execute_with(|| { + assert_eq!(System::inc_providers(&0), IncRefStatus::Created); assert_ok!(System::insert(&0, 42)); assert!(!System::is_provider_required(&0)); @@ -56,6 +57,7 @@ fn stored_map_works() { assert!(Killed::get().is_empty()); assert_ok!(System::remove(&0)); + assert_ok!(System::dec_providers(&0)); assert_eq!(Killed::get(), vec![0u64]); }); } diff --git a/frame/tips/src/tests.rs b/frame/tips/src/tests.rs index fa4ec15014..b2d97de183 100644 --- a/frame/tips/src/tests.rs +++ b/frame/tips/src/tests.rs @@ -97,6 +97,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } parameter_types! { static TenToFourteenTestValue: Vec = vec![10,11,12,13,14]; diff --git a/frame/transaction-payment/asset-tx-payment/src/lib.rs b/frame/transaction-payment/asset-tx-payment/src/lib.rs index b9cd6ef995..4e83d8b489 100644 --- a/frame/transaction-payment/asset-tx-payment/src/lib.rs +++ b/frame/transaction-payment/asset-tx-payment/src/lib.rs @@ -42,7 +42,7 @@ use frame_support::{ dispatch::{DispatchInfo, DispatchResult, PostDispatchInfo}, traits::{ tokens::{ - fungibles::{Balanced, CreditOf, Inspect}, + fungibles::{Balanced, Credit, Inspect}, WithdrawConsequence, }, IsType, @@ -104,7 +104,7 @@ pub enum InitialPayment { /// The initial fee was payed in the native currency. Native(LiquidityInfoOf), /// The initial fee was payed in an asset. - Asset(CreditOf), + Asset(Credit), } pub use pallet::*; @@ -159,7 +159,7 @@ where AssetBalanceOf: Send + Sync + FixedPointOperand, BalanceOf: Send + Sync + FixedPointOperand + IsType>, ChargeAssetIdOf: Send + Sync, - CreditOf: IsType>, + Credit: IsType>, { /// Utility constructor. Used only in client/factory code. pub fn from(tip: BalanceOf, asset_id: Option>) -> Self { @@ -216,7 +216,7 @@ where AssetBalanceOf: Send + Sync + FixedPointOperand, BalanceOf: Send + Sync + From + FixedPointOperand + IsType>, ChargeAssetIdOf: Send + Sync, - CreditOf: IsType>, + Credit: IsType>, { const IDENTIFIER: &'static str = "ChargeAssetTxPayment"; type AccountId = T::AccountId; diff --git a/frame/transaction-payment/asset-tx-payment/src/mock.rs b/frame/transaction-payment/asset-tx-payment/src/mock.rs index cd5147c3ac..be7baaf2b3 100644 --- a/frame/transaction-payment/asset-tx-payment/src/mock.rs +++ b/frame/transaction-payment/asset-tx-payment/src/mock.rs @@ -120,6 +120,10 @@ impl pallet_balances::Config for Runtime { type WeightInfo = (); type MaxReserves = ConstU32<50>; type ReserveIdentifier = [u8; 8]; + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } impl WeightToFeeT for WeightToFee { @@ -193,7 +197,7 @@ impl pallet_authorship::Config for Runtime { pub struct CreditToBlockAuthor; impl HandleCredit for CreditToBlockAuthor { - fn handle_credit(credit: CreditOf) { + fn handle_credit(credit: Credit) { if let Some(author) = pallet_authorship::Pallet::::author() { // What to do in case paying the author fails (e.g. because `fee < min_balance`) // default: drop the result which will trigger the `OnDrop` of the imbalance. diff --git a/frame/transaction-payment/asset-tx-payment/src/payment.rs b/frame/transaction-payment/asset-tx-payment/src/payment.rs index e273ffcfc5..f8d6888b99 100644 --- a/frame/transaction-payment/asset-tx-payment/src/payment.rs +++ b/frame/transaction-payment/asset-tx-payment/src/payment.rs @@ -20,8 +20,10 @@ use crate::Config; use codec::FullCodec; use frame_support::{ traits::{ - fungibles::{Balanced, CreditOf, Inspect}, - tokens::{Balance, BalanceConversion}, + fungibles::{Balanced, Credit, Inspect}, + tokens::{ + Balance, BalanceConversion, Fortitude::Polite, Precision::Exact, Preservation::Protect, + }, }, unsigned::TransactionValidityError, }; @@ -75,13 +77,13 @@ pub trait HandleCredit> { /// Implement to determine what to do with the withdrawn asset fees. /// Default for `CreditOf` from the assets pallet is to burn and /// decrease total issuance. - fn handle_credit(credit: CreditOf); + fn handle_credit(credit: Credit); } /// Default implementation that just drops the credit according to the `OnDrop` in the underlying /// imbalance type. impl> HandleCredit for () { - fn handle_credit(_credit: CreditOf) {} + fn handle_credit(_credit: Credit) {} } /// Implements the asset transaction for a balance to asset converter (implementing @@ -101,7 +103,7 @@ where { type Balance = BalanceOf; type AssetId = AssetIdOf; - type LiquidityInfo = CreditOf; + type LiquidityInfo = Credit; /// Withdraw the predicted fee from the transaction origin. /// @@ -126,8 +128,15 @@ where if !matches!(can_withdraw, WithdrawConsequence::Success) { return Err(InvalidTransaction::Payment.into()) } - >::withdraw(asset_id, who, converted_fee) - .map_err(|_| TransactionValidityError::from(InvalidTransaction::Payment)) + >::withdraw( + asset_id, + who, + converted_fee, + Exact, + Protect, + Polite, + ) + .map_err(|_| TransactionValidityError::from(InvalidTransaction::Payment)) } /// Hand the fee and the tip over to the `[HandleCredit]` implementation. diff --git a/frame/transaction-payment/asset-tx-payment/src/tests.rs b/frame/transaction-payment/asset-tx-payment/src/tests.rs index cd9891825d..2fee9c849f 100644 --- a/frame/transaction-payment/asset-tx-payment/src/tests.rs +++ b/frame/transaction-payment/asset-tx-payment/src/tests.rs @@ -28,7 +28,7 @@ use pallet_balances::Call as BalancesCall; use sp_runtime::traits::StaticLookup; const CALL: &::RuntimeCall = - &RuntimeCall::Balances(BalancesCall::transfer { dest: 2, value: 69 }); + &RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 2, value: 69 }); pub struct ExtBuilder { balance_factor: u64, diff --git a/frame/transaction-payment/src/mock.rs b/frame/transaction-payment/src/mock.rs index bfb9a194f8..741f094481 100644 --- a/frame/transaction-payment/src/mock.rs +++ b/frame/transaction-payment/src/mock.rs @@ -49,7 +49,7 @@ frame_support::construct_runtime!( ); pub(crate) const CALL: &::RuntimeCall = - &RuntimeCall::Balances(BalancesCall::transfer { dest: 2, value: 69 }); + &RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 2, value: 69 }); parameter_types! { pub(crate) static ExtrinsicBaseWeight: Weight = Weight::zero(); @@ -113,6 +113,10 @@ impl pallet_balances::Config for Runtime { type MaxReserves = (); type ReserveIdentifier = [u8; 8]; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } impl WeightToFeeT for WeightToFee { diff --git a/frame/transaction-payment/src/tests.rs b/frame/transaction-payment/src/tests.rs index 218f50e1cc..d5109609e2 100644 --- a/frame/transaction-payment/src/tests.rs +++ b/frame/transaction-payment/src/tests.rs @@ -285,7 +285,7 @@ fn signed_ext_length_fee_is_also_updated_per_congestion() { #[test] fn query_info_and_fee_details_works() { - let call = RuntimeCall::Balances(BalancesCall::transfer { dest: 2, value: 69 }); + let call = RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 2, value: 69 }); let origin = 111111; let extra = (); let xt = TestXt::new(call.clone(), Some((origin, extra))); @@ -348,7 +348,7 @@ fn query_info_and_fee_details_works() { #[test] fn query_call_info_and_fee_details_works() { - let call = RuntimeCall::Balances(BalancesCall::transfer { dest: 2, value: 69 }); + let call = RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest: 2, value: 69 }); let info = call.get_dispatch_info(); let encoded_call = call.encode(); let len = encoded_call.len() as u32; @@ -530,7 +530,11 @@ fn refund_does_not_recreate_account() { assert_eq!(Balances::free_balance(2), 200 - 5 - 10 - 100 - 5); // kill the account between pre and post dispatch - assert_ok!(Balances::transfer(Some(2).into(), 3, Balances::free_balance(2))); + assert_ok!(Balances::transfer_allow_death( + Some(2).into(), + 3, + Balances::free_balance(2) + )); assert_eq!(Balances::free_balance(2), 0); assert_ok!(ChargeTransactionPayment::::post_dispatch( diff --git a/frame/transaction-storage/src/mock.rs b/frame/transaction-storage/src/mock.rs index f54c134d74..3a87d8eaea 100644 --- a/frame/transaction-storage/src/mock.rs +++ b/frame/transaction-storage/src/mock.rs @@ -84,6 +84,10 @@ impl pallet_balances::Config for Test { type MaxLocks = (); type MaxReserves = (); type ReserveIdentifier = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } impl pallet_transaction_storage::Config for Test { diff --git a/frame/treasury/src/tests.rs b/frame/treasury/src/tests.rs index 24d2d01f92..67b21ff625 100644 --- a/frame/treasury/src/tests.rs +++ b/frame/treasury/src/tests.rs @@ -90,6 +90,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } impl pallet_utility::Config for Test { diff --git a/frame/uniques/src/mock.rs b/frame/uniques/src/mock.rs index 67d994d793..bad393a489 100644 --- a/frame/uniques/src/mock.rs +++ b/frame/uniques/src/mock.rs @@ -82,6 +82,10 @@ impl pallet_balances::Config for Test { type MaxLocks = (); type MaxReserves = ConstU32<50>; type ReserveIdentifier = [u8; 8]; + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } impl Config for Test { diff --git a/frame/utility/Cargo.toml b/frame/utility/Cargo.toml index 099526d3dc..c00255e3d3 100644 --- a/frame/utility/Cargo.toml +++ b/frame/utility/Cargo.toml @@ -47,5 +47,6 @@ runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-collective/runtime-benchmarks", ] try-runtime = ["frame-support/try-runtime"] diff --git a/frame/utility/src/tests.rs b/frame/utility/src/tests.rs index d9ac2ebbc1..04f2728242 100644 --- a/frame/utility/src/tests.rs +++ b/frame/utility/src/tests.rs @@ -35,6 +35,7 @@ use sp_core::H256; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, Hash, IdentityLookup}, + TokenError, }; type BlockNumber = u64; @@ -185,6 +186,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } impl pallet_root_testing::Config for Test {} @@ -226,7 +231,7 @@ impl Contains for TestBaseCallFilter { fn contains(c: &RuntimeCall) -> bool { match *c { // Transfer works. Use `transfer_keep_alive` for a call that doesn't pass the filter. - RuntimeCall::Balances(pallet_balances::Call::transfer { .. }) => true, + RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { .. }) => true, RuntimeCall::Utility(_) => true, // For benchmarking, this acts as a noop call RuntimeCall::System(frame_system::Call::remark { .. }) => true, @@ -253,7 +258,7 @@ type ExampleCall = example::Call; type UtilityCall = crate::Call; use frame_system::Call as SystemCall; -use pallet_balances::{Call as BalancesCall, Error as BalancesError}; +use pallet_balances::Call as BalancesCall; use pallet_root_testing::Call as RootTestingCall; use pallet_timestamp::Call as TimestampCall; @@ -278,7 +283,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities { } fn call_transfer(dest: u64, value: u64) -> RuntimeCall { - RuntimeCall::Balances(BalancesCall::transfer { dest, value }) + RuntimeCall::Balances(BalancesCall::transfer_allow_death { dest, value }) } fn call_foobar(err: bool, start_weight: Weight, end_weight: Option) -> RuntimeCall { @@ -289,10 +294,10 @@ fn call_foobar(err: bool, start_weight: Weight, end_weight: Option) -> R fn as_derivative_works() { new_test_ext().execute_with(|| { let sub_1_0 = Utility::derivative_account_id(1, 0); - assert_ok!(Balances::transfer(RuntimeOrigin::signed(1), sub_1_0, 5)); + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(1), sub_1_0, 5)); assert_err_ignore_postinfo!( Utility::as_derivative(RuntimeOrigin::signed(1), 1, Box::new(call_transfer(6, 3)),), - BalancesError::::InsufficientBalance + TokenError::FundsUnavailable, ); assert_ok!(Utility::as_derivative( RuntimeOrigin::signed(1), @@ -599,7 +604,7 @@ fn batch_all_revert() { ), pays_fee: Pays::Yes }, - error: pallet_balances::Error::::InsufficientBalance.into() + error: TokenError::FundsUnavailable.into(), } ); assert_eq!(Balances::free_balance(1), 10); diff --git a/frame/vesting/src/benchmarking.rs b/frame/vesting/src/benchmarking.rs index 35a094a9f4..15be519842 100644 --- a/frame/vesting/src/benchmarking.rs +++ b/frame/vesting/src/benchmarking.rs @@ -139,6 +139,7 @@ benchmarks! { let other: T::AccountId = account("other", 0, SEED); let other_lookup = T::Lookup::unlookup(other.clone()); + T::Currency::make_free_balance_be(&other, T::Currency::minimum_balance()); add_locks::(&other, l as u8); let expected_balance = add_vesting_schedules::(other_lookup.clone(), s)?; @@ -168,6 +169,7 @@ benchmarks! { let other: T::AccountId = account("other", 0, SEED); let other_lookup = T::Lookup::unlookup(other.clone()); + T::Currency::make_free_balance_be(&other, T::Currency::minimum_balance()); add_locks::(&other, l as u8); add_vesting_schedules::(other_lookup.clone(), s)?; // At block 21 everything is unlocked. @@ -200,8 +202,10 @@ benchmarks! { let target: T::AccountId = account("target", 0, SEED); let target_lookup = T::Lookup::unlookup(target.clone()); // Give target existing locks + T::Currency::make_free_balance_be(&target, T::Currency::minimum_balance()); add_locks::(&target, l as u8); // Add one vesting schedules. + let orig_balance = T::Currency::free_balance(&target); let mut expected_balance = add_vesting_schedules::(target_lookup.clone(), s)?; let transfer_amount = T::MinVestedTransfer::get(); @@ -216,7 +220,7 @@ benchmarks! { }: _(RawOrigin::Signed(caller), target_lookup, vesting_schedule) verify { assert_eq!( - expected_balance, + orig_balance + expected_balance, T::Currency::free_balance(&target), "Transfer didn't happen", ); @@ -238,8 +242,10 @@ benchmarks! { let target: T::AccountId = account("target", 0, SEED); let target_lookup = T::Lookup::unlookup(target.clone()); // Give target existing locks + T::Currency::make_free_balance_be(&target, T::Currency::minimum_balance()); add_locks::(&target, l as u8); // Add one less than max vesting schedules + let orig_balance = T::Currency::free_balance(&target); let mut expected_balance = add_vesting_schedules::(target_lookup.clone(), s)?; let transfer_amount = T::MinVestedTransfer::get(); @@ -254,7 +260,7 @@ benchmarks! { }: _(RawOrigin::Root, source_lookup, target_lookup, vesting_schedule) verify { assert_eq!( - expected_balance, + orig_balance + expected_balance, T::Currency::free_balance(&target), "Transfer didn't happen", ); @@ -272,6 +278,7 @@ benchmarks! { let caller: T::AccountId = account("caller", 0, SEED); let caller_lookup = T::Lookup::unlookup(caller.clone()); // Give target existing locks. + T::Currency::make_free_balance_be(&caller, T::Currency::minimum_balance()); add_locks::(&caller, l as u8); // Add max vesting schedules. let expected_balance = add_vesting_schedules::(caller_lookup, s)?; @@ -322,6 +329,7 @@ benchmarks! { let caller: T::AccountId = account("caller", 0, SEED); let caller_lookup = T::Lookup::unlookup(caller.clone()); // Give target other locks. + T::Currency::make_free_balance_be(&caller, T::Currency::minimum_balance()); add_locks::(&caller, l as u8); // Add max vesting schedules. let total_transferred = add_vesting_schedules::(caller_lookup, s)?; diff --git a/frame/vesting/src/mock.rs b/frame/vesting/src/mock.rs index f2ad6a7002..be6afd4a60 100644 --- a/frame/vesting/src/mock.rs +++ b/frame/vesting/src/mock.rs @@ -80,6 +80,10 @@ impl pallet_balances::Config for Test { type MaxReserves = (); type ReserveIdentifier = [u8; 8]; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } parameter_types! { pub const MinVestedTransfer: u64 = 256 * 2; diff --git a/frame/vesting/src/tests.rs b/frame/vesting/src/tests.rs index efc134cab7..46afe895f6 100644 --- a/frame/vesting/src/tests.rs +++ b/frame/vesting/src/tests.rs @@ -17,7 +17,10 @@ use frame_support::{assert_noop, assert_ok, assert_storage_noop, dispatch::EncodeLike}; use frame_system::RawOrigin; -use sp_runtime::traits::{BadOrigin, Identity}; +use sp_runtime::{ + traits::{BadOrigin, Identity}, + TokenError, +}; use super::{Vesting as VestingStorage, *}; use crate::mock::{Balances, ExtBuilder, System, Test, Vesting}; @@ -180,10 +183,8 @@ fn unvested_balance_should_not_transfer() { assert_eq!(user1_free_balance, 100); // Account 1 has free balance // Account 1 has only 5 units vested at block 1 (plus 50 unvested) assert_eq!(Vesting::vesting_balance(&1), Some(45)); - assert_noop!( - Balances::transfer(Some(1).into(), 2, 56), - pallet_balances::Error::::LiquidityRestrictions, - ); // Account 1 cannot send more than vested amount + // Account 1 cannot send more than vested amount... + assert_noop!(Balances::transfer_allow_death(Some(1).into(), 2, 56), TokenError::Frozen); }); } @@ -195,7 +196,7 @@ fn vested_balance_should_transfer() { // Account 1 has only 5 units vested at block 1 (plus 50 unvested) assert_eq!(Vesting::vesting_balance(&1), Some(45)); assert_ok!(Vesting::vest(Some(1).into())); - assert_ok!(Balances::transfer(Some(1).into(), 2, 55)); + assert_ok!(Balances::transfer_allow_death(Some(1).into(), 2, 55)); }); } @@ -213,7 +214,7 @@ fn vested_balance_should_transfer_with_multi_sched() { // Account 1 has only 256 units unlocking at block 1 (plus 1280 already fee). assert_eq!(Vesting::vesting_balance(&1), Some(2304)); assert_ok!(Vesting::vest(Some(1).into())); - assert_ok!(Balances::transfer(Some(1).into(), 2, 1536)); + assert_ok!(Balances::transfer_allow_death(Some(1).into(), 2, 1536)); }); } @@ -233,7 +234,7 @@ fn vested_balance_should_transfer_using_vest_other() { // Account 1 has only 5 units vested at block 1 (plus 50 unvested) assert_eq!(Vesting::vesting_balance(&1), Some(45)); assert_ok!(Vesting::vest_other(Some(2).into(), 1)); - assert_ok!(Balances::transfer(Some(1).into(), 2, 55)); + assert_ok!(Balances::transfer_allow_death(Some(1).into(), 2, 55)); }); } @@ -251,7 +252,7 @@ fn vested_balance_should_transfer_using_vest_other_with_multi_sched() { // Account 1 has only 256 units unlocking at block 1 (plus 1280 already free). assert_eq!(Vesting::vesting_balance(&1), Some(2304)); assert_ok!(Vesting::vest_other(Some(2).into(), 1)); - assert_ok!(Balances::transfer(Some(1).into(), 2, 1536)); + assert_ok!(Balances::transfer_allow_death(Some(1).into(), 2, 1536)); }); } @@ -266,8 +267,8 @@ fn non_vested_cannot_vest_other() { #[test] fn extra_balance_should_transfer() { ExtBuilder::default().existential_deposit(10).build().execute_with(|| { - assert_ok!(Balances::transfer(Some(3).into(), 1, 100)); - assert_ok!(Balances::transfer(Some(3).into(), 2, 100)); + assert_ok!(Balances::transfer_allow_death(Some(3).into(), 1, 100)); + assert_ok!(Balances::transfer_allow_death(Some(3).into(), 2, 100)); let user1_free_balance = Balances::free_balance(&1); assert_eq!(user1_free_balance, 200); // Account 1 has 100 more free balance than normal @@ -278,12 +279,13 @@ fn extra_balance_should_transfer() { // Account 1 has only 5 units vested at block 1 (plus 150 unvested) assert_eq!(Vesting::vesting_balance(&1), Some(45)); assert_ok!(Vesting::vest(Some(1).into())); - assert_ok!(Balances::transfer(Some(1).into(), 3, 155)); // Account 1 can send extra units gained + assert_ok!(Balances::transfer_allow_death(Some(1).into(), 3, 155)); // Account 1 can send extra units gained // Account 2 has no units vested at block 1, but gained 100 assert_eq!(Vesting::vesting_balance(&2), Some(200)); assert_ok!(Vesting::vest(Some(2).into())); - assert_ok!(Balances::transfer(Some(2).into(), 3, 100)); // Account 2 can send extra units gained + assert_ok!(Balances::transfer_allow_death(Some(2).into(), 3, 100)); // Account 2 can send extra + // units gained }); } @@ -305,7 +307,7 @@ fn liquid_funds_should_transfer_with_delayed_vesting() { assert_eq!(Vesting::vesting(&12).unwrap(), vec![user12_vesting_schedule]); // Account 12 can still send liquid funds - assert_ok!(Balances::transfer(Some(12).into(), 3, 256 * 5)); + assert_ok!(Balances::transfer_allow_death(Some(12).into(), 3, 256 * 5)); }); } @@ -1144,14 +1146,11 @@ fn vested_transfer_less_than_existential_deposit_fails() { ); // vested_transfer fails. - assert_noop!( - Vesting::vested_transfer(Some(3).into(), 99, sched), - pallet_balances::Error::::ExistentialDeposit, - ); + assert_noop!(Vesting::vested_transfer(Some(3).into(), 99, sched), TokenError::BelowMinimum,); // force_vested_transfer fails. assert_noop!( Vesting::force_vested_transfer(RawOrigin::Root.into(), 3, 99, sched), - pallet_balances::Error::::ExistentialDeposit, + TokenError::BelowMinimum, ); }); } diff --git a/frame/whitelist/src/mock.rs b/frame/whitelist/src/mock.rs index b16286863d..d644cd661e 100644 --- a/frame/whitelist/src/mock.rs +++ b/frame/whitelist/src/mock.rs @@ -86,6 +86,10 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = (); + type MaxHolds = (); } impl pallet_preimage::Config for Test { diff --git a/primitives/runtime/src/lib.rs b/primitives/runtime/src/lib.rs index dc03e074f9..622eac3d83 100644 --- a/primitives/runtime/src/lib.rs +++ b/primitives/runtime/src/lib.rs @@ -606,9 +606,10 @@ impl From for DispatchError { #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub enum TokenError { /// Funds are unavailable. - NoFunds, - /// Account that must exist would die. - WouldDie, + FundsUnavailable, + /// Some part of the balance gives the only provider reference to the account and thus cannot + /// be (re)moved. + OnlyProvider, /// Account cannot exist with the funds that would be given. BelowMinimum, /// Account cannot be created. @@ -619,18 +620,25 @@ pub enum TokenError { Frozen, /// Operation is not supported by the asset. Unsupported, + /// Account cannot be created for a held balance. + CannotCreateHold, + /// Withdrawal would cause unwanted loss of account. + NotExpendable, } impl From for &'static str { fn from(e: TokenError) -> &'static str { match e { - TokenError::NoFunds => "Funds are unavailable", - TokenError::WouldDie => "Account that must exist would die", + TokenError::FundsUnavailable => "Funds are unavailable", + TokenError::OnlyProvider => "Account that must exist would die", TokenError::BelowMinimum => "Account cannot exist with the funds that would be given", TokenError::CannotCreate => "Account cannot be created", TokenError::UnknownAsset => "The asset in question is unknown", TokenError::Frozen => "Funds exist but are frozen", TokenError::Unsupported => "Operation is not supported by the asset", + TokenError::CannotCreateHold => + "Account cannot be created for recording amount on hold", + TokenError::NotExpendable => "Account that is desired to remain would die", } } } @@ -994,8 +1002,8 @@ mod tests { Module(ModuleError { index: 2, error: [1, 0, 0, 0], message: None }), ConsumerRemaining, NoProviders, - Token(TokenError::NoFunds), - Token(TokenError::WouldDie), + Token(TokenError::FundsUnavailable), + Token(TokenError::OnlyProvider), Token(TokenError::BelowMinimum), Token(TokenError::CannotCreate), Token(TokenError::UnknownAsset),