diff --git a/asset-registry/src/mock/para.rs b/asset-registry/src/mock/para.rs index a64275d99..404cbb0a8 100644 --- a/asset-registry/src/mock/para.rs +++ b/asset-registry/src/mock/para.rs @@ -99,6 +99,7 @@ impl orml_tokens::Config for Runtime { type MaxReserves = (); type MaxLocks = ConstU32<50>; type DustRemovalWhitelist = Nothing; + type DustRemoval = (); } #[derive(scale_info::TypeInfo, Encode, Decode, Clone, Eq, PartialEq, Debug)] diff --git a/currencies/src/lib.rs b/currencies/src/lib.rs index f690c2718..209c49ae2 100644 --- a/currencies/src/lib.rs +++ b/currencies/src/lib.rs @@ -721,7 +721,7 @@ where Currency: PalletLockableCurrency, T: Config, { - type Moment = Moment; + type Moment = BlockNumberFor; fn set_lock(lock_id: LockIdentifier, who: &AccountId, amount: Self::Balance) -> DispatchResult { Currency::set_lock(lock_id, who, amount, WithdrawReasons::all()); diff --git a/currencies/src/mock.rs b/currencies/src/mock.rs index 27ee649b9..a1895c1be 100644 --- a/currencies/src/mock.rs +++ b/currencies/src/mock.rs @@ -80,7 +80,7 @@ impl MutationHooks, { - type OnDust = orml_tokens::TransferDust; + type OnDust = (); type OnSlash = (); type PreDeposit = (); type PostDeposit = (); @@ -102,6 +102,7 @@ impl orml_tokens::Config for Runtime { type MaxReserves = ConstU32<100_000>; type ReserveIdentifier = ReserveIdentifier; type DustRemovalWhitelist = Nothing; + type DustRemoval = (); } pub const NATIVE_CURRENCY_ID: CurrencyId = 1; diff --git a/payments/src/mock.rs b/payments/src/mock.rs index 82aac6335..e499cd1c5 100644 --- a/payments/src/mock.rs +++ b/payments/src/mock.rs @@ -97,6 +97,7 @@ impl orml_tokens::Config for Test { type DustRemovalWhitelist = MockDustRemovalWhitelist; type MaxReserves = ConstU32<2>; type ReserveIdentifier = ReserveIdentifier; + type DustRemoval = (); } pub struct MockDisputeResolver; diff --git a/tokens/Cargo.toml b/tokens/Cargo.toml index 6fa9c3e8f..0cc857d5c 100644 --- a/tokens/Cargo.toml +++ b/tokens/Cargo.toml @@ -26,6 +26,7 @@ pallet-treasury = { git = "https://github.com/paritytech/substrate", branch = "p sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v1.0.0" } sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v1.0.0" } sp-staking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v1.0.0" } +paste = "1.0.12" [features] default = ["std"] diff --git a/tokens/src/fungibles_conformance_tests/inspect_mutate.rs b/tokens/src/fungibles_conformance_tests/inspect_mutate.rs new file mode 100644 index 000000000..2210b5d24 --- /dev/null +++ b/tokens/src/fungibles_conformance_tests/inspect_mutate.rs @@ -0,0 +1,1119 @@ +use super::*; +use core::fmt::Debug; +use frame_support::traits::{ + fungibles::{Inspect, Mutate}, + tokens::{DepositConsequence, Fortitude, Precision, Preservation, Provenance, WithdrawConsequence}, +}; +use sp_arithmetic::traits::AtLeast8BitUnsigned; +use sp_runtime::traits::{Bounded, Zero}; + +/// Test the `mint_into` function for successful token minting. +/// +/// This test checks the `mint_into` function in the `Mutate` trait +/// implementation for type `T`. It ensures that account balances and total +/// issuance values are updated correctly after minting tokens into two distinct +/// accounts. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn mint_into_success(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account_0 = AccountId::from([10u8; 32]); + let account_1 = AccountId::from([11u8; 32]); + + // Test: Mint an amount into each account + let amount_0 = T::minimum_balance(asset_id); + let amount_1 = T::minimum_balance(asset_id) + 5.into(); + T::mint_into(asset_id, &account_0, amount_0).unwrap(); + T::mint_into(asset_id, &account_1, amount_1).unwrap(); + + // Verify: Account balances are updated correctly + assert_eq!(T::total_balance(asset_id, &account_0), amount_0); + assert_eq!(T::total_balance(asset_id, &account_1), amount_1); + assert_eq!(T::balance(asset_id, &account_0), amount_0); + assert_eq!(T::balance(asset_id, &account_1), amount_1); + + // Verify: Total issuance is updated correctly + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + amount_0 + amount_1 + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + amount_0 + amount_1 + ); +} + +/// Test the `mint_into` function for overflow prevention. +/// +/// This test ensures that minting tokens beyond the maximum balance value for +/// an account returns an error and does not change the account balance or total +/// issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn mint_into_overflow(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account = AccountId::from([10u8; 32]); + let amount = T::Balance::max_value() - 5.into() - initial_total_issuance; + + // Mint just below the maximum balance + T::mint_into(asset_id, &account, amount).unwrap(); + + // Verify: Minting beyond the maximum balance value returns an Err + T::mint_into(asset_id, &account, 10.into()).unwrap_err(); + + // Verify: The balance did not change + assert_eq!(T::total_balance(asset_id, &account), amount); + assert_eq!(T::balance(asset_id, &account), amount); + + // Verify: The total issuance did not change + assert_eq!(T::total_issuance(asset_id), initial_total_issuance + amount); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance + amount); +} + +/// Test the `mint_into` function for handling balances below the minimum value. +/// +/// This test verifies that minting tokens below the minimum balance for an +/// account returns an error and has no impact on the account balance or total +/// issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn mint_into_below_minimum( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + // Skip if there is no minimum balance + if T::minimum_balance(asset_id) == T::Balance::zero() { + return; + } + + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account = AccountId::from([10u8; 32]); + let amount = T::minimum_balance(asset_id) - 1.into(); + + // Verify: Minting below the minimum balance returns Err + T::mint_into(asset_id, &account, amount).unwrap_err(); + + // Verify: noop + assert_eq!(T::total_balance(asset_id, &account), T::Balance::zero()); + assert_eq!(T::balance(asset_id, &account), T::Balance::zero()); + assert_eq!(T::total_issuance(asset_id), initial_total_issuance); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance); +} + +/// Test the `burn_from` function for successfully burning an exact amount of +/// tokens. +/// +/// This test checks that the `burn_from` function with `Precision::Exact` +/// correctly reduces the account balance and total issuance values by the +/// burned amount. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate` for `AccountId`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn burn_from_exact_success( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + + // Setup account + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Test: Burn an exact amount from the account + let amount_to_burn = T::Balance::from(5); + let precision = Precision::Exact; + let force = Fortitude::Polite; + T::burn_from(asset_id, &account, amount_to_burn, precision, force).unwrap(); + + // Verify: The balance and total issuance should be reduced by the burned amount + assert_eq!(T::balance(asset_id, &account), initial_balance - amount_to_burn); + assert_eq!(T::total_balance(asset_id, &account), initial_balance - amount_to_burn); + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + initial_balance - amount_to_burn + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + initial_balance - amount_to_burn + ); +} + +/// Test the `burn_from` function for successfully burning tokens with a +/// best-effort approach. +/// +/// This test verifies that the `burn_from` function with +/// `Precision::BestEffort` correctly reduces the account balance and total +/// issuance values by the reducible balance when attempting to burn an amount +/// greater than the reducible balance. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate` for `AccountId`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn burn_from_best_effort_success( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + + // Setup account + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Get reducible balance + let force = Fortitude::Polite; + let reducible_balance = T::reducible_balance(asset_id, &account, Preservation::Expendable, force); + + // Test: Burn a best effort amount from the account that is greater than the + // reducible balance + let amount_to_burn = reducible_balance + 5.into(); + let precision = Precision::BestEffort; + assert!(amount_to_burn > reducible_balance); + assert!(amount_to_burn > T::balance(asset_id, &account)); + T::burn_from(asset_id, &account, amount_to_burn, precision, force).unwrap(); + + // Verify: The balance and total issuance should be reduced by the + // reducible_balance + assert_eq!(T::balance(asset_id, &account), initial_balance - reducible_balance); + assert_eq!( + T::total_balance(asset_id, &account), + initial_balance - reducible_balance + ); + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + initial_balance - reducible_balance + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + initial_balance - reducible_balance + ); +} + +/// Test the `burn_from` function for handling insufficient funds with +/// `Precision::Exact`. +/// +/// This test verifies that burning an amount greater than the account's balance +/// with `Precision::Exact` returns an error and does not change the account +/// balance or total issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn burn_from_exact_insufficient_funds( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + // Set up the initial conditions and parameters for the test + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + + // Verify: Burn an amount greater than the account's balance with Exact + // precision returns Err + let amount_to_burn = initial_balance + 10.into(); + let precision = Precision::Exact; + let force = Fortitude::Polite; + T::burn_from(asset_id, &account, amount_to_burn, precision, force).unwrap_err(); + + // Verify: The balance and total issuance should remain unchanged + assert_eq!(T::balance(asset_id, &account), initial_balance); + assert_eq!(T::total_balance(asset_id, &account), initial_balance); + assert_eq!(T::total_issuance(asset_id), initial_total_issuance); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance); +} + +/// Test the `restore` function for successful restoration. +/// +/// This test verifies that restoring an amount into each account updates their +/// balances and the total issuance values correctly. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn restore_success(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let account_0 = AccountId::from([10u8; 32]); + let account_1 = AccountId::from([11u8; 32]); + + // Test: Restore an amount into each account + let amount_0 = T::minimum_balance(asset_id); + let amount_1 = T::minimum_balance(asset_id) + 5.into(); + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + T::restore(asset_id, &account_0, amount_0).unwrap(); + T::restore(asset_id, &account_1, amount_1).unwrap(); + + // Verify: Account balances are updated correctly + assert_eq!(T::total_balance(asset_id, &account_0), amount_0); + assert_eq!(T::total_balance(asset_id, &account_1), amount_1); + assert_eq!(T::balance(asset_id, &account_0), amount_0); + assert_eq!(T::balance(asset_id, &account_1), amount_1); + + // Verify: Total issuance is updated correctly + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + amount_0 + amount_1 + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + amount_0 + amount_1 + ); +} + +/// Test the `restore` function for handling balance overflow. +/// +/// This test verifies that restoring an amount beyond the maximum balance +/// returns an error and does not change the account balance or total issuance +/// values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn restore_overflow(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account = AccountId::from([10u8; 32]); + let amount = T::Balance::max_value() - 5.into() - initial_total_issuance; + + // Restore just below the maximum balance + T::restore(asset_id, &account, amount).unwrap(); + + // Verify: Restoring beyond the maximum balance returns an Err + T::restore(asset_id, &account, 10.into()).unwrap_err(); + + // Verify: The balance and total issuance did not change + assert_eq!(T::total_balance(asset_id, &account), amount); + assert_eq!(T::balance(asset_id, &account), amount); + assert_eq!(T::total_issuance(asset_id), initial_total_issuance + amount); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance + amount); +} + +/// Test the `restore` function for handling restoration below the minimum +/// balance. +/// +/// This test verifies that restoring an amount below the minimum balance +/// returns an error and does not change the account balance or total issuance +/// values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn restore_below_minimum(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + // Skip if there is no minimum balance + if T::minimum_balance(asset_id) == T::Balance::zero() { + return; + } + + let account = AccountId::from([10u8; 32]); + let amount = T::minimum_balance(asset_id) - 1.into(); + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + + // Verify: Restoring below the minimum balance returns Err + T::restore(asset_id, &account, amount).unwrap_err(); + + // Verify: noop + assert_eq!(T::total_balance(asset_id, &account), T::Balance::zero()); + assert_eq!(T::balance(asset_id, &account), T::Balance::zero()); + assert_eq!(T::total_issuance(asset_id), initial_total_issuance); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance); +} + +/// Test the `shelve` function for successful shelving. +/// +/// This test verifies that shelving an amount from an account reduces the +/// account balance and total issuance values by the shelved amount. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn shelve_success(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + + // Setup account + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + + T::restore(asset_id, &account, initial_balance).unwrap(); + + // Test: Shelve an amount from the account + let amount_to_shelve = T::Balance::from(5); + T::shelve(asset_id, &account, amount_to_shelve).unwrap(); + + // Verify: The balance and total issuance should be reduced by the shelved + // amount + assert_eq!(T::balance(asset_id, &account), initial_balance - amount_to_shelve); + assert_eq!(T::total_balance(asset_id, &account), initial_balance - amount_to_shelve); + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + initial_balance - amount_to_shelve + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + initial_balance - amount_to_shelve + ); +} + +/// Test the `shelve` function for handling insufficient funds. +/// +/// This test verifies that attempting to shelve an amount greater than the +/// account's balance returns an error and does not change the account balance +/// or total issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn shelve_insufficient_funds( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + + // Set up the initial conditions and parameters for the test + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::restore(asset_id, &account, initial_balance).unwrap(); + + // Verify: Shelving greater than the balance with Exact precision returns Err + let amount_to_shelve = initial_balance + 10.into(); + T::shelve(asset_id, &account, amount_to_shelve).unwrap_err(); + + // Verify: The balance and total issuance should remain unchanged + assert_eq!(T::balance(asset_id, &account), initial_balance); + assert_eq!(T::total_balance(asset_id, &account), initial_balance); + assert_eq!(T::total_issuance(asset_id), initial_total_issuance + initial_balance); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance + initial_balance); +} + +/// Test the `transfer` function for a successful transfer. +/// +/// This test verifies that transferring an amount between two accounts with +/// `Preservation::Expendable` updates the account balances and maintains the +/// total issuance and active issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn transfer_success(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account_0 = AccountId::from([10u8; 32]); + let account_1 = AccountId::from([11u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::set_balance(asset_id, &account_0, initial_balance); + T::set_balance(asset_id, &account_1, initial_balance); + + // Test: Transfer an amount from account_0 to account_1 + let transfer_amount = T::Balance::from(3); + T::transfer( + asset_id, + &account_0, + &account_1, + transfer_amount, + Preservation::Expendable, + ) + .unwrap(); + + // Verify: Account balances are updated correctly + assert_eq!( + T::total_balance(asset_id, &account_0), + initial_balance - transfer_amount + ); + assert_eq!( + T::total_balance(asset_id, &account_1), + initial_balance + transfer_amount + ); + assert_eq!(T::balance(asset_id, &account_0), initial_balance - transfer_amount); + assert_eq!(T::balance(asset_id, &account_1), initial_balance + transfer_amount); + + // Verify: Total issuance doesn't change + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + initial_balance * 2.into() + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + initial_balance * 2.into() + ); +} + +/// Test the `transfer` function with `Preservation::Expendable` for +/// transferring the entire balance. +/// +/// This test verifies that transferring the entire balance from one account to +/// another with `Preservation::Expendable` updates the account balances and +/// maintains the total issuance and active issuance values. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn transfer_expendable_all( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account_0 = AccountId::from([10u8; 32]); + let account_1 = AccountId::from([11u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::set_balance(asset_id, &account_0, initial_balance); + T::set_balance(asset_id, &account_1, initial_balance); + + // Test: Transfer entire balance from account_0 to account_1 + let preservation = Preservation::Expendable; + let transfer_amount = initial_balance; + T::transfer(asset_id, &account_0, &account_1, transfer_amount, preservation).unwrap(); + + // Verify: Account balances are updated correctly + assert_eq!(T::total_balance(asset_id, &account_0), T::Balance::zero()); + assert_eq!(T::total_balance(asset_id, &account_1), initial_balance * 2.into()); + assert_eq!(T::balance(asset_id, &account_0), T::Balance::zero()); + assert_eq!(T::balance(asset_id, &account_1), initial_balance * 2.into()); + + // Verify: Total issuance doesn't change + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + initial_balance * 2.into() + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + initial_balance * 2.into() + ); +} + +/// Test the transfer function with Preservation::Expendable for transferring +/// amounts that leaves an account with less than the minimum balance. +/// +/// This test verifies that when transferring an amount using +/// Preservation::Expendable and an account will be left with less than the +/// minimum balance, the account balances are updated, dust is collected +/// properly depending on whether a dust_trap exists, and the total issuance and +/// active issuance values remain consistent. +/// +/// # Parameters +/// +/// - dust_trap: An optional account identifier to which dust will be collected. +/// If None, dust will be removed from the total and active issuance. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn transfer_expendable_dust( + asset_id: >::AssetId, + dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + if T::minimum_balance(asset_id) == T::Balance::zero() { + return; + } + + let account_0 = AccountId::from([10u8; 32]); + let account_1 = AccountId::from([20u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::set_balance(asset_id, &account_0, initial_balance); + T::set_balance(asset_id, &account_1, initial_balance); + + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let initial_dust_trap_balance = match dust_trap { + Some(ref dust_trap) => T::total_balance(asset_id, &dust_trap), + None => T::Balance::zero(), + }; + + // Test: Transfer balance + let preservation = Preservation::Expendable; + let transfer_amount = T::Balance::from(11); + T::transfer(asset_id, &account_0, &account_1, transfer_amount, preservation).unwrap(); + + // Verify: Account balances are updated correctly + assert_eq!(T::total_balance(asset_id, &account_0), T::Balance::zero()); + assert_eq!( + T::total_balance(asset_id, &account_1), + initial_balance + transfer_amount + ); + assert_eq!(T::balance(asset_id, &account_0), T::Balance::zero()); + assert_eq!(T::balance(asset_id, &account_1), initial_balance + transfer_amount); + + match dust_trap { + Some(ref dust_trap) => { + // Verify: Total issuance and active issuance don't change + assert_eq!(T::total_issuance(asset_id), initial_total_issuance); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance); + // Verify: Dust is collected into dust trap + assert_eq!( + T::total_balance(asset_id, &dust_trap), + initial_dust_trap_balance + T::minimum_balance(asset_id) - 1.into() + ); + assert_eq!( + T::balance(asset_id, &dust_trap), + initial_dust_trap_balance + T::minimum_balance(asset_id) - 1.into() + ); + } + None => { + // Verify: Total issuance and active issuance are reduced by the dust amount + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance - T::minimum_balance(asset_id) + 1.into() + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance - T::minimum_balance(asset_id) + 1.into() + ); + } + } +} + +/// Test the `transfer` function with `Preservation::Protect` and +/// `Preservation::Preserve` for transferring the entire balance. +/// +/// This test verifies that attempting to transfer the entire balance with +/// `Preservation::Protect` or `Preservation::Preserve` returns an error, and +/// the account balances, total issuance, and active issuance values remain +/// unchanged. +/// +/// # Type Parameters +/// +/// ```text +/// - `T`: Implements `Mutate`. +/// - `AccountId`: Account identifier implementing `AtLeast8BitUnsigned`. +/// ``` +pub fn transfer_protect_preserve( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + // This test means nothing if there is no minimum balance + if T::minimum_balance(asset_id) == T::Balance::zero() { + return; + } + + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account_0 = AccountId::from([10u8; 32]); + let account_1 = AccountId::from([11u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::set_balance(asset_id, &account_0, initial_balance); + T::set_balance(asset_id, &account_1, initial_balance); + + // Verify: Transfer Protect entire balance from account_0 to account_1 should + // Err + let preservation = Preservation::Protect; + let transfer_amount = initial_balance; + T::transfer(asset_id, &account_0, &account_1, transfer_amount, preservation).unwrap_err(); + + // Verify: Noop + assert_eq!(T::total_balance(asset_id, &account_0), initial_balance); + assert_eq!(T::total_balance(asset_id, &account_1), initial_balance); + assert_eq!(T::balance(asset_id, &account_0), initial_balance); + assert_eq!(T::balance(asset_id, &account_1), initial_balance); + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + initial_balance * 2.into() + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + initial_balance * 2.into() + ); + + // Verify: Transfer Preserve entire balance from account_0 to account_1 should + // Err + let preservation = Preservation::Preserve; + T::transfer(asset_id, &account_0, &account_1, transfer_amount, preservation).unwrap_err(); + + // Verify: Noop + assert_eq!(T::total_balance(asset_id, &account_0), initial_balance); + assert_eq!(T::total_balance(asset_id, &account_1), initial_balance); + assert_eq!(T::balance(asset_id, &account_0), initial_balance); + assert_eq!(T::balance(asset_id, &account_1), initial_balance); + assert_eq!( + T::total_issuance(asset_id), + initial_total_issuance + initial_balance * 2.into() + ); + assert_eq!( + T::active_issuance(asset_id), + initial_active_issuance + initial_balance * 2.into() + ); +} + +/// Test the set_balance function for successful minting. +/// +/// This test verifies that minting a balance using set_balance updates the +/// account balance, total issuance, and active issuance correctly. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn set_balance_mint_success( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Test: Increase the account balance with set_balance + let increase_amount: T::Balance = 5.into(); + let new = T::set_balance(asset_id, &account, initial_balance + increase_amount); + + // Verify: set_balance returned the new balance + let expected_new = initial_balance + increase_amount; + assert_eq!(new, expected_new); + + // Verify: Balance and issuance is updated correctly + assert_eq!(T::total_balance(asset_id, &account), expected_new); + assert_eq!(T::balance(asset_id, &account), expected_new); + assert_eq!(T::total_issuance(asset_id), initial_total_issuance + expected_new); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance + expected_new); +} + +/// Test the set_balance function for successful burning. +/// +/// This test verifies that burning a balance using set_balance updates the +/// account balance, total issuance, and active issuance correctly. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn set_balance_burn_success( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let initial_total_issuance = T::total_issuance(asset_id); + let initial_active_issuance = T::active_issuance(asset_id); + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Test: Increase the account balance with set_balance + let burn_amount: T::Balance = 5.into(); + let new = T::set_balance(asset_id, &account, initial_balance - burn_amount); + + // Verify: set_balance returned the new balance + let expected_new = initial_balance - burn_amount; + assert_eq!(new, expected_new); + + // Verify: Balance and issuance is updated correctly + assert_eq!(T::total_balance(asset_id, &account), expected_new); + assert_eq!(T::balance(asset_id, &account), expected_new); + assert_eq!(T::total_issuance(asset_id), initial_total_issuance + expected_new); + assert_eq!(T::active_issuance(asset_id), initial_active_issuance + expected_new); +} + +/// Test the can_deposit function for returning a success value. +/// +/// This test verifies that the can_deposit function returns +/// DepositConsequence::Success when depositing a reasonable amount. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_deposit_success(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Test: can_deposit a reasonable amount + let ret = T::can_deposit(asset_id, &account, 5.into(), Provenance::Minted); + + // Verify: Returns success + assert_eq!(ret, DepositConsequence::Success); +} + +/// Test the can_deposit function for returning a minimum balance error. +/// +/// This test verifies that the can_deposit function returns +/// DepositConsequence::BelowMinimum when depositing below the minimum balance. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_deposit_below_minimum( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + // can_deposit always returns Success for amount 0 + if T::minimum_balance(asset_id) < 2.into() { + return; + } + + let account = AccountId::from([10u8; 32]); + + // Test: can_deposit below the minimum + let ret = T::can_deposit( + asset_id, + &account, + T::minimum_balance(asset_id) - 1.into(), + Provenance::Minted, + ); + + // Verify: Returns success + assert_eq!(ret, DepositConsequence::BelowMinimum); +} + +/// Test the can_deposit function for returning an overflow error. +/// +/// This test verifies that the can_deposit function returns +/// DepositConsequence::Overflow when depositing an amount that would cause an +/// overflow. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_deposit_overflow(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let account = AccountId::from([10u8; 32]); + + // Test: Try deposit over the max balance + let initial_balance = T::Balance::max_value() - 5.into() - T::total_issuance(asset_id); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + let ret = T::can_deposit(asset_id, &account, 10.into(), Provenance::Minted); + + // Verify: Returns success + assert_eq!(ret, DepositConsequence::Overflow); +} + +/// Test the can_withdraw function for returning a success value. +/// +/// This test verifies that the can_withdraw function returns +/// WithdrawConsequence::Success when withdrawing a reasonable amount. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_withdraw_success(asset_id: >::AssetId, _dust_trap: Option) +where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Test: can_withdraw a reasonable amount + let ret = T::can_withdraw(asset_id, &account, 5.into()); + + // Verify: Returns success + assert_eq!(ret, WithdrawConsequence::Success); +} + +/// Test the can_withdraw function for withdrawal resulting in a reduced balance +/// of zero. +/// +/// This test verifies that the can_withdraw function returns +/// WithdrawConsequence::ReducedToZero when withdrawing an amount that would +/// reduce the account balance below the minimum balance. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_withdraw_reduced_to_zero( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + if T::minimum_balance(asset_id) == T::Balance::zero() { + return; + } + + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Verify: can_withdraw below the minimum balance returns ReducedToZero + let ret = T::can_withdraw(asset_id, &account, 1.into()); + assert_eq!( + ret, + WithdrawConsequence::ReducedToZero(T::minimum_balance(asset_id) - 1.into()) + ); +} + +/// Test the can_withdraw function for returning a low balance error. +/// +/// This test verifies that the can_withdraw function returns +/// WithdrawConsequence::BalanceLow when withdrawing an amount that would result +/// in an account balance below the current balance. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn can_withdraw_balance_low( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + if T::minimum_balance(asset_id) == T::Balance::zero() { + return; + } + + let account = AccountId::from([10u8; 32]); + let other_account = AccountId::from([100u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 5.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + T::mint_into(asset_id, &other_account, initial_balance * 2.into()).unwrap(); + + // Verify: can_withdraw below the account balance returns BalanceLow + let ret = T::can_withdraw(asset_id, &account, initial_balance + 1.into()); + assert_eq!(ret, WithdrawConsequence::BalanceLow); +} + +/// Test the reducible_balance function with Preservation::Expendable. +/// +/// This test verifies that the reducible_balance function returns the full +/// account balance when using Preservation::Expendable. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn reducible_balance_expendable( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Verify: reducible_balance returns the full balance + let ret = T::reducible_balance(asset_id, &account, Preservation::Expendable, Fortitude::Polite); + assert_eq!(ret, initial_balance); +} + +/// Test the reducible_balance function with Preservation::Protect and +/// Preservation::Preserve. +/// +/// This test verifies that the reducible_balance function returns the account +/// balance minus the minimum balance when using either Preservation::Protect or +/// Preservation::Preserve. +/// +/// # Type Parameters +/// +/// ```text +/// - T: Implements Mutate. +/// - AccountId: Account identifier implementing AtLeast8BitUnsigned. +/// ``` +pub fn reducible_balance_protect_preserve( + asset_id: >::AssetId, + _dust_trap: Option, +) where + T: Mutate, + >::AssetId: Copy, + >::Balance: AtLeast8BitUnsigned + Debug, + AccountId: From<[u8; 32]>, +{ + let account = AccountId::from([10u8; 32]); + let initial_balance = T::minimum_balance(asset_id) + 10.into(); + T::mint_into(asset_id, &account, initial_balance).unwrap(); + + // Verify: reducible_balance returns the full balance - min balance + let ret = T::reducible_balance(asset_id, &account, Preservation::Protect, Fortitude::Polite); + assert_eq!(ret, initial_balance - T::minimum_balance(asset_id)); + let ret = T::reducible_balance(asset_id, &account, Preservation::Preserve, Fortitude::Polite); + assert_eq!(ret, initial_balance - T::minimum_balance(asset_id)); +} diff --git a/tokens/src/fungibles_conformance_tests/mod.rs b/tokens/src/fungibles_conformance_tests/mod.rs new file mode 100644 index 000000000..291223473 --- /dev/null +++ b/tokens/src/fungibles_conformance_tests/mod.rs @@ -0,0 +1,3 @@ +#![cfg(test)] + +pub mod inspect_mutate; diff --git a/tokens/src/imbalances.rs b/tokens/src/imbalances.rs deleted file mode 100644 index 6aef35cd5..000000000 --- a/tokens/src/imbalances.rs +++ /dev/null @@ -1,174 +0,0 @@ -// wrapping these imbalances in a private module is necessary to ensure absolute -// privacy of the inner member. -use crate::{Config, TotalIssuance}; -use frame_support::traits::{Get, Imbalance, SameOrOther, TryDrop}; -use sp_runtime::traits::{Saturating, Zero}; -use sp_std::{marker, mem, result}; - -/// 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] -pub struct PositiveImbalance>( - T::Balance, - marker::PhantomData, -); - -impl> PositiveImbalance { - /// Create a new positive imbalance from a balance. - pub fn new(amount: T::Balance) -> Self { - PositiveImbalance(amount, marker::PhantomData::) - } -} - -impl> Default for PositiveImbalance { - fn default() -> Self { - Self::zero() - } -} - -/// 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] -pub struct NegativeImbalance>( - T::Balance, - marker::PhantomData, -); - -impl> NegativeImbalance { - /// Create a new negative imbalance from a balance. - pub fn new(amount: T::Balance) -> Self { - NegativeImbalance(amount, marker::PhantomData::) - } -} - -impl> Default for NegativeImbalance { - fn default() -> Self { - Self::zero() - } -} - -impl> TryDrop for PositiveImbalance { - fn try_drop(self) -> result::Result<(), Self> { - self.drop_zero() - } -} - -impl> Imbalance for PositiveImbalance { - type Opposite = NegativeImbalance; - - fn zero() -> Self { - Self::new(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.saturating_sub(first); - - mem::forget(self); - (Self::new(first), Self::new(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); - } - // allow to make the impl same with `pallet-balances` - #[allow(clippy::comparison_chain)] - fn offset(self, other: Self::Opposite) -> SameOrOther { - let (a, b) = (self.0, other.0); - mem::forget((self, other)); - - if a > b { - SameOrOther::Same(Self::new(a.saturating_sub(b))) - } else if b > a { - SameOrOther::Other(NegativeImbalance::new(b.saturating_sub(a))) - } else { - SameOrOther::None - } - } - fn peek(&self) -> T::Balance { - self.0 - } -} - -impl> TryDrop for NegativeImbalance { - fn try_drop(self) -> result::Result<(), Self> { - self.drop_zero() - } -} - -impl> Imbalance for NegativeImbalance { - type Opposite = PositiveImbalance; - - fn zero() -> Self { - Self::new(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.saturating_sub(first); - - mem::forget(self); - (Self::new(first), Self::new(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); - } - // allow to make the impl same with `pallet-balances` - #[allow(clippy::comparison_chain)] - fn offset(self, other: Self::Opposite) -> SameOrOther { - let (a, b) = (self.0, other.0); - mem::forget((self, other)); - - if a > b { - SameOrOther::Same(Self::new(a.saturating_sub(b))) - } else if b > a { - SameOrOther::Other(PositiveImbalance::new(b.saturating_sub(a))) - } else { - SameOrOther::None - } - } - fn peek(&self) -> T::Balance { - self.0 - } -} - -impl> Drop for PositiveImbalance { - /// Basic drop handler will just square up the total issuance. - fn drop(&mut self) { - TotalIssuance::::mutate(GetCurrencyId::get(), |v| *v = v.saturating_add(self.0)); - } -} - -impl> Drop for NegativeImbalance { - /// Basic drop handler will just square up the total issuance. - fn drop(&mut self) { - TotalIssuance::::mutate(GetCurrencyId::get(), |v| *v = v.saturating_sub(self.0)); - } -} diff --git a/tokens/src/impl_currency.rs b/tokens/src/impl_currency.rs new file mode 100644 index 000000000..a90f29f8a --- /dev/null +++ b/tokens/src/impl_currency.rs @@ -0,0 +1,750 @@ +// wrapping these imbalances in a private module is necessary to ensure absolute +// privacy of the inner member. +use super::*; +use frame_support::traits::{SameOrOther, TryDrop}; +use sp_std::{marker, mem, result}; + +/// 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] +pub struct PositiveImbalance>( + T::Balance, + marker::PhantomData, +); + +impl> PositiveImbalance { + /// Create a new positive imbalance from a balance. + pub fn new(amount: T::Balance) -> Self { + PositiveImbalance(amount, marker::PhantomData::) + } +} + +impl> Default for PositiveImbalance { + fn default() -> Self { + Self::zero() + } +} + +/// 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] +pub struct NegativeImbalance>( + T::Balance, + marker::PhantomData, +); + +impl> NegativeImbalance { + /// Create a new negative imbalance from a balance. + pub fn new(amount: T::Balance) -> Self { + NegativeImbalance(amount, marker::PhantomData::) + } +} + +impl> Default for NegativeImbalance { + fn default() -> Self { + Self::zero() + } +} + +impl> TryDrop for PositiveImbalance { + fn try_drop(self) -> result::Result<(), Self> { + self.drop_zero() + } +} + +impl> Imbalance for PositiveImbalance { + type Opposite = NegativeImbalance; + + fn zero() -> Self { + Self::new(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.saturating_sub(first); + + mem::forget(self); + (Self::new(first), Self::new(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); + } + // allow to make the impl same with `pallet-balances` + #[allow(clippy::comparison_chain)] + fn offset(self, other: Self::Opposite) -> SameOrOther { + let (a, b) = (self.0, other.0); + mem::forget((self, other)); + + if a > b { + SameOrOther::Same(Self::new(a.saturating_sub(b))) + } else if b > a { + SameOrOther::Other(NegativeImbalance::new(b.saturating_sub(a))) + } else { + SameOrOther::None + } + } + fn peek(&self) -> T::Balance { + self.0 + } +} + +impl> TryDrop for NegativeImbalance { + fn try_drop(self) -> result::Result<(), Self> { + self.drop_zero() + } +} + +impl> Imbalance for NegativeImbalance { + type Opposite = PositiveImbalance; + + fn zero() -> Self { + Self::new(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.saturating_sub(first); + + mem::forget(self); + (Self::new(first), Self::new(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); + } + // allow to make the impl same with `pallet-balances` + #[allow(clippy::comparison_chain)] + fn offset(self, other: Self::Opposite) -> SameOrOther { + let (a, b) = (self.0, other.0); + mem::forget((self, other)); + + if a > b { + SameOrOther::Same(Self::new(a.saturating_sub(b))) + } else if b > a { + SameOrOther::Other(PositiveImbalance::new(b.saturating_sub(a))) + } else { + SameOrOther::None + } + } + fn peek(&self) -> T::Balance { + self.0 + } +} + +impl> Drop for PositiveImbalance { + /// Basic drop handler will just square up the total issuance. + fn drop(&mut self) { + TotalIssuance::::mutate(GetCurrencyId::get(), |v| *v = v.saturating_add(self.0)); + } +} + +impl> Drop for NegativeImbalance { + /// Basic drop handler will just square up the total issuance. + fn drop(&mut self) { + TotalIssuance::::mutate(GetCurrencyId::get(), |v| *v = v.saturating_sub(self.0)); + } +} + +/// Implementation of Currency traits for Tokens Module. +impl MultiCurrency for Pallet { + type CurrencyId = T::CurrencyId; + type Balance = T::Balance; + + fn minimum_balance(currency_id: Self::CurrencyId) -> Self::Balance { + Self::ed(currency_id) + } + + fn total_issuance(currency_id: Self::CurrencyId) -> Self::Balance { + Self::total_issuance(currency_id) + } + + fn total_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { + Self::accounts(who, currency_id).total() + } + + fn free_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { + Self::accounts(who, currency_id).free + } + + fn ensure_can_withdraw(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + Self::ensure_can_withdraw(currency_id, who, amount) + } + + fn transfer( + currency_id: Self::CurrencyId, + from: &T::AccountId, + to: &T::AccountId, + amount: Self::Balance, + ) -> DispatchResult { + // allow death + Self::do_transfer(currency_id, from, to, amount, ExistenceRequirement::AllowDeath) + } + + fn deposit(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + // do not require existing + // need change total issuance + Self::do_deposit(currency_id, who, amount, false, true)?; + Ok(()) + } + + fn withdraw(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + // allow death + // need change total issuance + Self::do_withdraw(currency_id, who, amount, ExistenceRequirement::AllowDeath, true) + } + + // Check if `value` amount of free balance can be slashed from `who`. + fn can_slash(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> bool { + if value.is_zero() { + return true; + } + Self::free_balance(currency_id, who) >= value + } + + /// Is a no-op if `value` to be slashed is zero. + /// + /// NOTE: `slash()` just slash free balance. + fn slash(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> Self::Balance { + if amount.is_zero() { + return amount; + } + + >::OnSlash::on_slash( + currency_id, + who, + amount, + ); + Self::mutate_account_handling_dust(currency_id, who, |account| -> Self::Balance { + let free_slashed_amount = account.free.min(amount); + account.free = account.free.defensive_saturating_sub(free_slashed_amount); + + // Cannot underflow because the slashed value cannot be greater than total + // issuance but just to be defensive here. + TotalIssuance::::mutate(currency_id, |v| *v = v.defensive_saturating_sub(free_slashed_amount)); + + Self::deposit_event(Event::Slashed { + currency_id, + who: who.clone(), + free_amount: free_slashed_amount, + reserved_amount: Zero::zero(), + }); + + amount.saturating_sub(free_slashed_amount) + }) + } +} + +impl MultiCurrencyExtended for Pallet { + type Amount = T::Amount; + + fn update_balance(currency_id: Self::CurrencyId, who: &T::AccountId, by_amount: Self::Amount) -> DispatchResult { + if by_amount.is_zero() { + return Ok(()); + } + + // Ensure this doesn't overflow. There isn't any traits that exposes + // `saturating_abs` so we need to do it manually. + let by_amount_abs = if by_amount == Self::Amount::min_value() { + Self::Amount::max_value() + } else { + by_amount.abs() + }; + + let by_balance = + TryInto::::try_into(by_amount_abs).map_err(|_| Error::::AmountIntoBalanceFailed)?; + if by_amount.is_positive() { + Self::deposit(currency_id, who, by_balance) + } else { + Self::withdraw(currency_id, who, by_balance).map(|_| ()) + } + } +} + +impl MultiLockableCurrency for Pallet { + type Moment = BlockNumberFor; + + // Set a lock on the balance of `who` under `currency_id`. + // Is a no-op if lock amount is zero. + fn set_lock( + lock_id: LockIdentifier, + currency_id: Self::CurrencyId, + who: &T::AccountId, + amount: Self::Balance, + ) -> DispatchResult { + if amount.is_zero() { + return Ok(()); + } + let mut new_lock = Some(BalanceLock { id: lock_id, amount }); + let mut locks = Self::locks(who, currency_id) + .into_iter() + .filter_map(|lock| { + if lock.id == lock_id { + new_lock.take() + } else { + Some(lock) + } + }) + .collect::>(); + if let Some(lock) = new_lock { + locks.push(lock) + } + Self::update_locks(currency_id, who, &locks[..])?; + + Self::deposit_event(Event::LockSet { + lock_id, + currency_id, + who: who.clone(), + amount, + }); + Ok(()) + } + + // Extend a lock on the balance of `who` under `currency_id`. + // Is a no-op if lock amount is zero + fn extend_lock( + lock_id: LockIdentifier, + currency_id: Self::CurrencyId, + who: &T::AccountId, + amount: Self::Balance, + ) -> DispatchResult { + if amount.is_zero() { + return Ok(()); + } + let mut new_lock = Some(BalanceLock { id: lock_id, amount }); + let mut locks = Self::locks(who, currency_id) + .into_iter() + .filter_map(|lock| { + if lock.id == lock_id { + new_lock.take().map(|nl| BalanceLock { + id: lock.id, + amount: lock.amount.max(nl.amount), + }) + } else { + Some(lock) + } + }) + .collect::>(); + if let Some(lock) = new_lock { + locks.push(lock) + } + Self::update_locks(currency_id, who, &locks[..]) + } + + fn remove_lock(lock_id: LockIdentifier, currency_id: Self::CurrencyId, who: &T::AccountId) -> DispatchResult { + let mut locks = Self::locks(who, currency_id); + locks.retain(|lock| lock.id != lock_id); + let locks_vec = locks.to_vec(); + Self::update_locks(currency_id, who, &locks_vec[..])?; + + Self::deposit_event(Event::LockRemoved { + lock_id, + currency_id, + who: who.clone(), + }); + Ok(()) + } +} + +impl MultiReservableCurrency for Pallet { + /// Check if `who` can reserve `value` from their free balance. + /// + /// Always `true` if value to be reserved is zero. + fn can_reserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> bool { + if value.is_zero() { + return true; + } + Self::ensure_can_withdraw(currency_id, who, value).is_ok() + } + + /// Slash from reserved balance, returning any amount that was unable to + /// be slashed. + /// + /// Is a no-op if the value to be slashed is zero. + fn slash_reserved(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> Self::Balance { + if value.is_zero() { + return value; + } + + >::OnSlash::on_slash( + currency_id, + who, + value, + ); + Self::mutate_account_handling_dust(currency_id, who, |account| -> Self::Balance { + let reserved_slashed_amount = account.reserved.min(value); + account.reserved = account.reserved.defensive_saturating_sub(reserved_slashed_amount); + + // Cannot underflow because the slashed value cannot be greater than total + // issuance but just to be defensive here. + TotalIssuance::::mutate(currency_id, |v| { + *v = v.defensive_saturating_sub(reserved_slashed_amount) + }); + + Self::deposit_event(Event::Slashed { + currency_id, + who: who.clone(), + free_amount: Zero::zero(), + reserved_amount: reserved_slashed_amount, + }); + + value.saturating_sub(reserved_slashed_amount) + }) + } + + fn reserved_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { + Self::accounts(who, currency_id).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(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> DispatchResult { + if value.is_zero() { + return Ok(()); + } + Self::ensure_can_withdraw(currency_id, who, value)?; + + Self::mutate_account_handling_dust(currency_id, who, |account| { + account.free = account.free.defensive_saturating_sub(value); + account.reserved = account.reserved.defensive_saturating_add(value); + + Self::deposit_event(Event::Reserved { + currency_id, + 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. + /// + /// NOTE: returns amount value which wasn't successfully unreserved. + fn unreserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> Self::Balance { + if value.is_zero() { + return Zero::zero(); + } + + let actual = Self::mutate_account_handling_dust(currency_id, who, |account| { + let actual = account.reserved.min(value); + account.reserved = account.reserved.defensive_saturating_sub(actual); + account.free = account.free.defensive_saturating_add(actual); + + Self::deposit_event(Event::Unreserved { + currency_id, + who: who.clone(), + amount: actual, + }); + actual + }); + + value.defensive_saturating_sub(actual) + } + + /// 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( + currency_id: Self::CurrencyId, + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: BalanceStatus, + ) -> sp_std::result::Result { + let actual = Self::do_transfer_reserved( + currency_id, + slashed, + beneficiary, + value, + Precision::BestEffort, + Fortitude::Polite, + status, + )?; + Ok(value.saturating_sub(actual)) + } +} + +impl NamedMultiReservableCurrency for Pallet { + type ReserveIdentifier = T::ReserveIdentifier; + + fn reserved_balance_named( + id: &Self::ReserveIdentifier, + currency_id: Self::CurrencyId, + who: &T::AccountId, + ) -> Self::Balance { + let reserves = Self::reserves(who, currency_id); + 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, + currency_id: Self::CurrencyId, + who: &T::AccountId, + value: Self::Balance, + ) -> DispatchResult { + if value.is_zero() { + return Ok(()); + } + + Reserves::::try_mutate(who, currency_id, |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(currency_id, who, value) + }) + } + + /// 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, + currency_id: Self::CurrencyId, + who: &T::AccountId, + value: Self::Balance, + ) -> Self::Balance { + if value.is_zero() { + return Zero::zero(); + } + + Reserves::::mutate_exists(who, currency_id, |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(currency_id, 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`, but just to be defensive + // here. + reserves[index].amount = reserves[index].amount.defensive_saturating_sub(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.defensive_saturating_sub(actual) + } + Err(_) => value, + } + } else { + value + } + }) + } + + /// Slash from reserved balance, returning the 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, + currency_id: Self::CurrencyId, + who: &T::AccountId, + value: Self::Balance, + ) -> Self::Balance { + if value.is_zero() { + return Zero::zero(); + } + + Reserves::::mutate(who, currency_id, |reserves| -> Self::Balance { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + let to_change = cmp::min(reserves[index].amount, value); + + let remain = >::slash_reserved(currency_id, 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` but just to be defensive + // here. + reserves[index].amount = reserves[index].amount.defensive_saturating_sub(actual); + + Self::deposit_event(Event::Slashed { + who: who.clone(), + currency_id, + free_amount: Zero::zero(), + reserved_amount: actual, + }); + value.defensive_saturating_sub(actual) + } + Err(_) => 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, + currency_id: Self::CurrencyId, + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: BalanceStatus, + ) -> Result { + if value.is_zero() { + return Ok(Zero::zero()); + } + + if slashed == beneficiary { + return match status { + BalanceStatus::Free => Ok(Self::unreserve_named(id, currency_id, slashed, value)), + BalanceStatus::Reserved => { + Ok(value.saturating_sub(Self::reserved_balance_named(id, currency_id, slashed))) + } + }; + } + + Reserves::::try_mutate( + slashed, + currency_id, + |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 == BalanceStatus::Reserved { + // make it the reserved under same identifier + Reserves::::try_mutate( + beneficiary, + currency_id, + |reserves| -> Result { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + let remain = >::repatriate_reserved( + currency_id, + 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( + currency_id, + 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( + currency_id, + 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` but just to be defensive + // here. + reserves[index].amount = reserves[index].amount.defensive_saturating_sub(actual); + Ok(value.defensive_saturating_sub(actual)) + } + Err(_) => Ok(value), + } + }, + ) + } +} diff --git a/tokens/src/impl_fungibles.rs b/tokens/src/impl_fungibles.rs new file mode 100644 index 000000000..e5aec3b31 --- /dev/null +++ b/tokens/src/impl_fungibles.rs @@ -0,0 +1,452 @@ +//! Implementation of `fungibles` traits for Tokens Module. +use super::*; + +impl fungibles::Inspect for Pallet { + type AssetId = T::CurrencyId; + type Balance = T::Balance; + + fn total_issuance(asset_id: Self::AssetId) -> Self::Balance { + TotalIssuance::::get(asset_id) + } + + fn minimum_balance(asset_id: Self::AssetId) -> Self::Balance { + Self::ed(asset_id) + } + + fn total_balance(asset_id: Self::AssetId, who: &T::AccountId) -> Self::Balance { + Self::accounts(who, asset_id).total() + } + + fn balance(asset_id: Self::AssetId, who: &T::AccountId) -> Self::Balance { + Self::accounts(who, asset_id).free + } + + /// 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_id: Self::AssetId, + who: &T::AccountId, + preservation: Preservation, + force: Fortitude, + ) -> Self::Balance { + let a = Self::accounts(who, asset_id); + let mut untouchable = Zero::zero(); + if force == Fortitude::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 == Preservation::Preserve + // ..or we don't want the account to die and our provider ref is needed for it to live.. + || preservation == 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 == Preservation::Expendable && !a.free.is_zero() && + !frame_system::Pallet::::can_dec_provider(who) + { + // ..then the ED needed except for the account in dust removal whitelist. + if !Self::in_dust_removal_whitelist(who) { + untouchable = untouchable.max(Self::ed(asset_id)); + } + } + // Liquid balance is what is neither on hold nor frozen/required for provider. + a.free.saturating_sub(untouchable) + } + + fn can_deposit( + asset_id: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + provenance: Provenance, + ) -> DepositConsequence { + if amount.is_zero() { + return DepositConsequence::Success; + } + + if provenance == Provenance::Minted && TotalIssuance::::get(asset_id).checked_add(&amount).is_none() { + return DepositConsequence::Overflow; + } + + let account = Self::accounts(who, asset_id); + let new_free_balance = match account.free.checked_add(&amount) { + Some(x) if x < Self::ed(asset_id) && !Self::in_dust_removal_whitelist(who) => { + return DepositConsequence::BelowMinimum + } + Some(x) => x, + None => return DepositConsequence::Overflow, + }; + + match account.reserved.checked_add(&new_free_balance) { + 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( + asset_id: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + ) -> WithdrawConsequence { + if amount.is_zero() { + return WithdrawConsequence::Success; + } + + if TotalIssuance::::get(asset_id).checked_sub(&amount).is_none() { + return WithdrawConsequence::Underflow; + } + + let account = Self::accounts(who, asset_id); + let new_free_balance = match account.free.checked_sub(&amount) { + Some(x) => x, + None => return WithdrawConsequence::BalanceLow, + }; + + let liquid = Self::reducible_balance(asset_id, who, Preservation::Expendable, Fortitude::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 = Self::ed(asset_id); + let success = if new_free_balance < ed && !Self::in_dust_removal_whitelist(who) { + 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 total funds must be no less than the frozen balance. + if new_total_balance < account.frozen { + return WithdrawConsequence::Frozen; + } + + success + } + + fn asset_exists(asset: Self::AssetId) -> bool { + TotalIssuance::::contains_key(asset) + } +} + +impl fungibles::Unbalanced for Pallet { + fn handle_dust(dust: fungibles::Dust) { + T::DustRemoval::on_unbalanced(dust.into_credit()); + } + + /// 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_id: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + ) -> Result, DispatchError> { + let max_reduction = >::reducible_balance( + asset_id, + who, + Preservation::Expendable, + Fortitude::Force, + ); + let (result, maybe_dust) = Self::mutate_account(asset_id, 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::::BalanceTooLow); + + account.free = amount; + Ok(()) + }); + result?; + Ok(maybe_dust) + } + + fn set_total_issuance(asset_id: Self::AssetId, amount: Self::Balance) { + // Balance is the same type and will not overflow + TotalIssuance::::mutate(asset_id, |t| *t = 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. + /// NOTE: this impl overrides the default implementation of + /// fungibles::Unbalanced, allow `amount < Self::minimum_balance() && who is + /// in DustRemovalWhitelist` when the account of `who` is zero + fn increase_balance( + asset: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + let old_balance = >::balance(asset, who); + let new_balance = if let Precision::BestEffort = precision { + old_balance.saturating_add(amount) + } else { + old_balance.checked_add(&amount).ok_or(ArithmeticError::Overflow)? + }; + if new_balance < >::minimum_balance(asset) + && !Self::in_dust_removal_whitelist(who) + { + // Attempt to increase from 0 to below minimum -> stays at zero. + if let Precision::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(fungibles::Dust(asset, dust)); + } + Ok(new_balance.saturating_sub(old_balance)) + } + } +} + +impl fungibles::Balanced for Pallet { + type OnDropCredit = fungibles::DecreaseIssuance; + type OnDropDebt = fungibles::IncreaseIssuance; + + fn done_deposit(asset: Self::AssetId, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Deposited { + currency_id: asset, + who: who.clone(), + amount, + }); + } + + fn done_withdraw(asset: Self::AssetId, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Withdrawn { + currency_id: asset, + who: who.clone(), + amount, + }); + } + + fn done_issue(asset: Self::AssetId, amount: Self::Balance) { + Self::deposit_event(Event::Issued { + currency_id: asset, + amount, + }); + } + + fn done_rescind(asset: Self::AssetId, amount: Self::Balance) { + Self::deposit_event(Event::Rescinded { + currency_id: asset, + amount, + }); + } +} + +impl fungibles::Mutate for Pallet { + fn done_mint_into(asset: Self::AssetId, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Deposited { + currency_id: asset, + who: who.clone(), + amount, + }); + } + + fn done_burn_from(asset: Self::AssetId, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Withdrawn { + currency_id: asset, + who: who.clone(), + amount, + }); + } + + fn done_shelve(asset: Self::AssetId, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Withdrawn { + currency_id: asset, + who: who.clone(), + amount, + }); + } + + fn done_restore(asset: Self::AssetId, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Deposited { + currency_id: asset, + who: who.clone(), + amount, + }); + } + + fn done_transfer(asset: Self::AssetId, source: &T::AccountId, dest: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Transfer { + currency_id: asset, + from: source.clone(), + to: dest.clone(), + amount, + }); + } +} + +impl fungibles::InspectHold for Pallet { + type Reason = (); + + fn total_balance_on_hold(asset: Self::AssetId, who: &T::AccountId) -> Self::Balance { + Self::accounts(who, asset).reserved + } + + /// 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: &T::AccountId, force: Fortitude) -> Self::Balance { + // The total balance must never drop below the freeze requirements if we're not + // forcing: + let a = Self::accounts(who, asset); + let unavailable = if force == Fortitude::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(asset_id: Self::AssetId, _reason: &Self::Reason, who: &T::AccountId) -> T::Balance { + Self::accounts(who, asset_id).reserved + } + + fn hold_available(_asset: Self::AssetId, _reason: &Self::Reason, _who: &T::AccountId) -> bool { + true + } +} + +impl fungibles::UnbalancedHold for Pallet { + /// 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: &T::AccountId, + amount: Self::Balance, + ) -> DispatchResult { + // Balance is the same type and will not overflow + let (result, maybe_dust) = Self::try_mutate_account(asset, who, |account, _| -> Result<(), DispatchError> { + let old_reserved = account.reserved; + let delta = old_reserved.max(amount) - old_reserved.min(amount); + + account.reserved = if amount > old_reserved { + account.reserved.checked_add(&delta).ok_or(ArithmeticError::Overflow)? + } else { + account.reserved.checked_sub(&delta).ok_or(ArithmeticError::Underflow)? + }; + + Ok(()) + })?; + + debug_assert!( + maybe_dust.is_none(), + "Does not alter main balance; dust only happens when it is altered; qed" + ); + + Ok(result) + } +} + +impl fungibles::BalancedHold for Pallet { + fn done_slash(asset: Self::AssetId, _reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Slashed { + currency_id: asset, + who: who.clone(), + free_amount: amount, + reserved_amount: Zero::zero(), + }); + } +} + +impl fungibles::MutateHold for Pallet { + fn done_hold(asset: Self::AssetId, _reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Reserved { + currency_id: asset, + who: who.clone(), + amount, + }); + } + fn done_release(asset: Self::AssetId, _reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Unreserved { + currency_id: asset, + who: who.clone(), + amount, + }); + } + fn done_burn_held(asset: Self::AssetId, _reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Slashed { + currency_id: asset, + who: who.clone(), + free_amount: Zero::zero(), + reserved_amount: amount, + }); + } + fn done_transfer_on_hold( + asset: Self::AssetId, + _reason: &Self::Reason, + source: &T::AccountId, + dest: &T::AccountId, + amount: Self::Balance, + ) { + // TODO: fungibles::MutateHold::transfer_on_hold did not pass the mode to this + // hook, use `BalanceStatus::Reserved` temporarily, need to fix it + Self::deposit_event(Event::::ReserveRepatriated { + currency_id: asset, + from: source.clone(), + to: dest.clone(), + amount, + status: BalanceStatus::Reserved, + }); + } +} + +// TODO: impl fungibles::InspectFreeze and fungibles::MutateFreeze diff --git a/tokens/src/impls.rs b/tokens/src/impls.rs index 9bbccc777..c5731e0ff 100644 --- a/tokens/src/impls.rs +++ b/tokens/src/impls.rs @@ -1,11 +1,30 @@ -use frame_support::dispatch::DispatchError; -use frame_support::traits::tokens::{Fortitude, Precision, Preservation, Provenance}; -use frame_support::traits::{ - fungible, fungibles, - tokens::{Balance as BalanceT, DepositConsequence, WithdrawConsequence}, - Contains, Get, +use super::*; +use frame_support::{ + traits::{ + tokens::Balance as BalanceT, Currency as PalletCurrency, LockableCurrency as PalletLockableCurrency, + NamedReservableCurrency as PalletNamedReservableCurrency, ReservableCurrency as PalletReservableCurrency, + SignedImbalance, WithdrawReasons, + }, + transactional, }; -use sp_arithmetic::{traits::Bounded, ArithmeticError}; + +pub type CreditOf = fungibles::Credit<::AccountId, Pallet>; +pub struct DustReceiver(sp_std::marker::PhantomData<(T, GetAccountId)>); +impl OnUnbalanced> for DustReceiver +where + T: Config, + GetAccountId: Get>, +{ + fn on_nonzero_unbalanced(amount: CreditOf) { + match GetAccountId::get() { + None => drop(amount), + Some(receiver) => { + let result = as fungibles::Balanced<_>>::resolve(&receiver, amount); + debug_assert!(result.is_ok()); + } + } + } +} pub struct Combiner(sp_std::marker::PhantomData<(AccountId, TestKey, A, B)>); @@ -150,9 +169,16 @@ where A: fungible::Mutate>::Balance>, B: fungibles::Mutate, { - fn handle_dust(_dust: fungibles::Dust) { - // FIXME: only way to access internals of Dust is into_credit, but T is - // not balanced + fn handle_dust(dust: fungibles::Dust) { + let asset = dust.0; + let dust_amount = dust.1; + if TestKey::contains(&asset) { + let fungible_dust = fungible::Dust::(dust_amount); + A::handle_dust(fungible_dust) + } else { + let fungibles_dust = fungibles::Dust::(asset, dust_amount); + B::handle_dust(fungibles_dust) + } } fn write_balance( @@ -167,6 +193,22 @@ where } } + /// NOTE: this impl overrides the default implementation of + /// fungibles::Unbalanced, because orml-tokens override the default the + /// implementation of fungibles::Unbalanced. Here override for consistency. + fn increase_balance( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + if TestKey::contains(&asset) { + A::increase_balance(who, amount, precision) + } else { + B::increase_balance(asset, who, amount, precision) + } + } + fn set_total_issuance(asset: Self::AssetId, amount: Self::Balance) { if TestKey::contains(&asset) { A::set_total_issuance(amount) @@ -331,16 +373,586 @@ where B: BalanceT, GetCurrencyId: Get<>::AssetId>, { - fn handle_dust(_dust: fungible::Dust) { - // FIXME: only way to access internals of Dust is into_credit, but T is - // not balanced + fn handle_dust(dust: fungible::Dust) { + let dust_amount = dust.0; + let asset = GetCurrencyId::get(); + let fungibles_dust = fungibles::Dust::(asset, dust_amount); + T::handle_dust(fungibles_dust) } fn write_balance(who: &AccountId, amount: Self::Balance) -> Result, DispatchError> { T::write_balance(GetCurrencyId::get(), who, amount) } + /// NOTE: this impl overrides the default implementation of + /// fungible::Unbalanced, because orml-tokens override the default the + /// implementation of fungibles::Unbalanced. Here override for consistency. + fn increase_balance( + who: &AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + T::increase_balance(GetCurrencyId::get(), who, amount, precision) + } + fn set_total_issuance(amount: Self::Balance) { T::set_total_issuance(GetCurrencyId::get(), amount) } } + +// The adapter for specific token, which implements the +// frame_support::traits::Currency traits, orml Currency traits and fungible +// traits. +pub struct CurrencyAdapter(marker::PhantomData<(T, GetCurrencyId)>); + +impl PalletCurrency for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type Balance = T::Balance; + type PositiveImbalance = PositiveImbalance; + type NegativeImbalance = NegativeImbalance; + + fn total_balance(who: &T::AccountId) -> Self::Balance { + as MultiCurrency<_>>::total_balance(GetCurrencyId::get(), who) + } + + fn can_slash(who: &T::AccountId, value: Self::Balance) -> bool { + as MultiCurrency<_>>::can_slash(GetCurrencyId::get(), who, value) + } + + fn total_issuance() -> Self::Balance { + as MultiCurrency<_>>::total_issuance(GetCurrencyId::get()) + } + + fn minimum_balance() -> Self::Balance { + as MultiCurrency<_>>::minimum_balance(GetCurrencyId::get()) + } + + fn burn(mut amount: Self::Balance) -> Self::PositiveImbalance { + if amount.is_zero() { + return PositiveImbalance::zero(); + } + let currency_id = GetCurrencyId::get(); + TotalIssuance::::mutate(currency_id, |issued| { + *issued = issued.checked_sub(&amount).unwrap_or_else(|| { + amount = *issued; + Zero::zero() + }) + }); + + Pallet::::deposit_event(Event::TotalIssuanceSet { + currency_id, + amount: Self::total_issuance(), + }); + PositiveImbalance::new(amount) + } + + fn issue(mut amount: Self::Balance) -> Self::NegativeImbalance { + if amount.is_zero() { + return NegativeImbalance::zero(); + } + TotalIssuance::::mutate(GetCurrencyId::get(), |issued| { + *issued = issued.checked_add(&amount).unwrap_or_else(|| { + amount = Self::Balance::max_value().defensive_saturating_sub(*issued); + Self::Balance::max_value() + }) + }); + + Pallet::::deposit_event(Event::TotalIssuanceSet { + currency_id: GetCurrencyId::get(), + amount: Self::total_issuance(), + }); + NegativeImbalance::new(amount) + } + + fn free_balance(who: &T::AccountId) -> Self::Balance { + as MultiCurrency<_>>::free_balance(GetCurrencyId::get(), who) + } + + fn ensure_can_withdraw( + who: &T::AccountId, + amount: Self::Balance, + _reasons: WithdrawReasons, + _new_balance: Self::Balance, + ) -> DispatchResult { + as MultiCurrency<_>>::ensure_can_withdraw(GetCurrencyId::get(), who, amount) + } + + fn transfer( + source: &T::AccountId, + dest: &T::AccountId, + value: Self::Balance, + existence_requirement: ExistenceRequirement, + ) -> DispatchResult { + Pallet::::do_transfer(GetCurrencyId::get(), source, dest, value, existence_requirement) + } + + fn slash(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { + if value.is_zero() { + return (Self::NegativeImbalance::zero(), value); + } + + let currency_id = GetCurrencyId::get(); + >::OnSlash::on_slash( + currency_id, + who, + value, + ); + let (actual, remaining_slash) = + Pallet::::mutate_account_handling_dust(currency_id, who, |account| -> (Self::Balance, Self::Balance) { + let free_slashed_amount = account.free.min(value); + account.free = account.free.defensive_saturating_sub(free_slashed_amount); + + Pallet::::deposit_event(Event::Slashed { + currency_id, + who: who.clone(), + free_amount: free_slashed_amount, + reserved_amount: Zero::zero(), + }); + + (free_slashed_amount, value.saturating_sub(free_slashed_amount)) + }); + + (Self::NegativeImbalance::new(actual), remaining_slash) + } + + /// Deposit some `value` into the free balance of an existing target account + /// `who`. + fn deposit_into_existing( + who: &T::AccountId, + value: Self::Balance, + ) -> sp_std::result::Result { + // do not change total issuance + Pallet::::do_deposit(GetCurrencyId::get(), who, value, true, false).map(|_| PositiveImbalance::new(value)) + } + + /// Deposit some `value` into the free balance of `who`, possibly creating a + /// new account. + fn deposit_creating(who: &T::AccountId, value: Self::Balance) -> Self::PositiveImbalance { + // do not change total issuance + Pallet::::do_deposit(GetCurrencyId::get(), who, value, false, false) + .map_or_else(|_| Self::PositiveImbalance::zero(), |_| PositiveImbalance::new(value)) + } + + fn withdraw( + who: &T::AccountId, + value: Self::Balance, + _reasons: WithdrawReasons, + liveness: ExistenceRequirement, + ) -> sp_std::result::Result { + // do not change total issuance + Pallet::::do_withdraw(GetCurrencyId::get(), who, value, liveness, false) + .map(|_| Self::NegativeImbalance::new(value)) + } + + fn make_free_balance_be( + who: &T::AccountId, + value: Self::Balance, + ) -> SignedImbalance { + let currency_id = GetCurrencyId::get(); + Pallet::::try_mutate_account_handling_dust( + currency_id, + who, + |account, is_new| -> Result, ()> { + // 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. + let ed = T::ExistentialDeposits::get(¤cy_id); + ensure!(value >= ed || T::DustRemovalWhitelist::contains(who) || !is_new, ()); + + let imbalance = if account.free <= value { + SignedImbalance::Positive(PositiveImbalance::new(value.saturating_sub(account.free))) + } else { + SignedImbalance::Negative(NegativeImbalance::new(account.free.saturating_sub(value))) + }; + account.free = value; + + Pallet::::deposit_event(Event::BalanceSet { + currency_id, + who: who.clone(), + free: value, + reserved: account.reserved, + }); + Ok(imbalance) + }, + ) + .unwrap_or_else(|_| SignedImbalance::Positive(Self::PositiveImbalance::zero())) + } +} + +impl PalletReservableCurrency for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn can_reserve(who: &T::AccountId, value: Self::Balance) -> bool { + as MultiReservableCurrency<_>>::can_reserve(GetCurrencyId::get(), who, value) + } + + fn slash_reserved(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { + let actual = as MultiReservableCurrency<_>>::slash_reserved(GetCurrencyId::get(), who, value); + (Self::NegativeImbalance::zero(), actual) + } + + fn reserved_balance(who: &T::AccountId) -> Self::Balance { + as MultiReservableCurrency<_>>::reserved_balance(GetCurrencyId::get(), who) + } + + fn reserve(who: &T::AccountId, value: Self::Balance) -> DispatchResult { + as MultiReservableCurrency<_>>::reserve(GetCurrencyId::get(), who, value) + } + + fn unreserve(who: &T::AccountId, value: Self::Balance) -> Self::Balance { + as MultiReservableCurrency<_>>::unreserve(GetCurrencyId::get(), who, value) + } + + fn repatriate_reserved( + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: BalanceStatus, + ) -> sp_std::result::Result { + as MultiReservableCurrency<_>>::repatriate_reserved( + GetCurrencyId::get(), + slashed, + beneficiary, + value, + status, + ) + } +} + +impl PalletNamedReservableCurrency for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type ReserveIdentifier = T::ReserveIdentifier; + + fn reserved_balance_named(id: &Self::ReserveIdentifier, who: &T::AccountId) -> Self::Balance { + as NamedMultiReservableCurrency<_>>::reserved_balance_named(id, GetCurrencyId::get(), who) + } + + fn reserve_named(id: &Self::ReserveIdentifier, who: &T::AccountId, value: Self::Balance) -> DispatchResult { + as NamedMultiReservableCurrency<_>>::reserve_named(id, GetCurrencyId::get(), who, value) + } + + fn unreserve_named(id: &Self::ReserveIdentifier, who: &T::AccountId, value: Self::Balance) -> Self::Balance { + as NamedMultiReservableCurrency<_>>::unreserve_named(id, GetCurrencyId::get(), who, value) + } + + fn slash_reserved_named( + id: &Self::ReserveIdentifier, + who: &T::AccountId, + value: Self::Balance, + ) -> (Self::NegativeImbalance, Self::Balance) { + let actual = + as NamedMultiReservableCurrency<_>>::slash_reserved_named(id, GetCurrencyId::get(), who, value); + (Self::NegativeImbalance::zero(), actual) + } + + fn repatriate_reserved_named( + id: &Self::ReserveIdentifier, + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: BalanceStatus, + ) -> sp_std::result::Result { + as NamedMultiReservableCurrency<_>>::repatriate_reserved_named( + id, + GetCurrencyId::get(), + slashed, + beneficiary, + value, + status, + ) + } +} + +impl PalletLockableCurrency for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type Moment = BlockNumberFor; + type MaxLocks = (); + + fn set_lock(id: LockIdentifier, who: &T::AccountId, amount: Self::Balance, _reasons: WithdrawReasons) { + let _ = as MultiLockableCurrency<_>>::set_lock(id, GetCurrencyId::get(), who, amount); + } + + fn extend_lock(id: LockIdentifier, who: &T::AccountId, amount: Self::Balance, _reasons: WithdrawReasons) { + let _ = as MultiLockableCurrency<_>>::extend_lock(id, GetCurrencyId::get(), who, amount); + } + + fn remove_lock(id: LockIdentifier, who: &T::AccountId) { + let _ = as MultiLockableCurrency<_>>::remove_lock(id, GetCurrencyId::get(), who); + } +} + +impl TransferAll for Pallet { + #[transactional] + fn transfer_all(source: &T::AccountId, dest: &T::AccountId) -> DispatchResult { + Accounts::::iter_prefix(source).try_for_each(|(currency_id, account_data)| -> DispatchResult { + // allow death + Self::do_transfer( + currency_id, + source, + dest, + account_data.free, + ExistenceRequirement::AllowDeath, + ) + }) + } +} + +impl fungible::Inspect for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type Balance = T::Balance; + + fn total_issuance() -> Self::Balance { + as fungibles::Inspect<_>>::total_issuance(GetCurrencyId::get()) + } + fn minimum_balance() -> Self::Balance { + as fungibles::Inspect<_>>::minimum_balance(GetCurrencyId::get()) + } + fn balance(who: &T::AccountId) -> Self::Balance { + as fungibles::Inspect<_>>::balance(GetCurrencyId::get(), who) + } + fn total_balance(who: &T::AccountId) -> Self::Balance { + as fungibles::Inspect<_>>::total_balance(GetCurrencyId::get(), who) + } + fn reducible_balance(who: &T::AccountId, preservation: Preservation, fortitude: Fortitude) -> Self::Balance { + as fungibles::Inspect<_>>::reducible_balance(GetCurrencyId::get(), who, preservation, fortitude) + } + fn can_deposit(who: &T::AccountId, amount: Self::Balance, provenance: Provenance) -> DepositConsequence { + as fungibles::Inspect<_>>::can_deposit(GetCurrencyId::get(), who, amount, provenance) + } + fn can_withdraw(who: &T::AccountId, amount: Self::Balance) -> WithdrawConsequence { + as fungibles::Inspect<_>>::can_withdraw(GetCurrencyId::get(), who, amount) + } +} + +impl fungible::Mutate for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn done_mint_into(who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Deposited { + currency_id: GetCurrencyId::get(), + who: who.clone(), + amount, + }); + } + + fn done_burn_from(who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Withdrawn { + currency_id: GetCurrencyId::get(), + who: who.clone(), + amount, + }); + } + + fn done_shelve(who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Withdrawn { + currency_id: GetCurrencyId::get(), + who: who.clone(), + amount, + }); + } + + fn done_restore(who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Deposited { + currency_id: GetCurrencyId::get(), + who: who.clone(), + amount, + }); + } + + fn done_transfer(source: &T::AccountId, dest: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Transfer { + currency_id: GetCurrencyId::get(), + from: source.clone(), + to: dest.clone(), + amount, + }); + } +} + +impl fungible::Unbalanced for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn handle_dust(dust: fungible::Dust) { + let dust_amount = dust.0; + let asset = GetCurrencyId::get(); + let fungibles_dust = fungibles::Dust::>(asset, dust_amount); + as fungibles::Unbalanced<_>>::handle_dust(fungibles_dust) + } + + fn write_balance(who: &T::AccountId, amount: Self::Balance) -> Result, DispatchError> { + as fungibles::Unbalanced<_>>::write_balance(GetCurrencyId::get(), who, amount) + } + + fn set_total_issuance(amount: Self::Balance) { + as fungibles::Unbalanced<_>>::set_total_issuance(GetCurrencyId::get(), amount) + } + + /// NOTE: this impl overrides the default implementation of + /// fungible::Unbalanced, because orml-tokens override the default the + /// implementation of fungibles::Unbalanced. Here override for consistency. + fn increase_balance( + who: &T::AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + as fungibles::Unbalanced<_>>::increase_balance(GetCurrencyId::get(), who, amount, precision) + } +} + +impl fungible::Balanced for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type OnDropCredit = fungible::DecreaseIssuance; + type OnDropDebt = fungible::IncreaseIssuance; + + fn done_deposit(who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Deposited { + currency_id: GetCurrencyId::get(), + who: who.clone(), + amount, + }); + } + + fn done_withdraw(who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Withdrawn { + currency_id: GetCurrencyId::get(), + who: who.clone(), + amount, + }); + } + + fn done_issue(amount: Self::Balance) { + Pallet::::deposit_event(Event::Issued { + currency_id: GetCurrencyId::get(), + amount, + }); + } + + fn done_rescind(amount: Self::Balance) { + Pallet::::deposit_event(Event::Rescinded { + currency_id: GetCurrencyId::get(), + amount, + }); + } +} + +impl fungible::InspectHold for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type Reason = as fungibles::InspectHold>::Reason; + + fn balance_on_hold(reason: &Self::Reason, who: &T::AccountId) -> T::Balance { + as fungibles::InspectHold<_>>::balance_on_hold(GetCurrencyId::get(), reason, who) + } + fn total_balance_on_hold(who: &T::AccountId) -> Self::Balance { + as fungibles::InspectHold<_>>::total_balance_on_hold(GetCurrencyId::get(), who) + } + fn reducible_total_balance_on_hold(who: &T::AccountId, force: Fortitude) -> Self::Balance { + as fungibles::InspectHold<_>>::reducible_total_balance_on_hold(GetCurrencyId::get(), who, force) + } + fn hold_available(reason: &Self::Reason, who: &T::AccountId) -> bool { + as fungibles::InspectHold<_>>::hold_available(GetCurrencyId::get(), reason, who) + } + fn can_hold(reason: &Self::Reason, who: &T::AccountId, amount: T::Balance) -> bool { + as fungibles::InspectHold<_>>::can_hold(GetCurrencyId::get(), reason, who, amount) + } +} + +impl fungible::MutateHold for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn done_hold(_reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Reserved { + currency_id: GetCurrencyId::get(), + who: who.clone(), + amount, + }); + } + + fn done_release(_reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Unreserved { + currency_id: GetCurrencyId::get(), + who: who.clone(), + amount, + }); + } + + fn done_burn_held(_reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Slashed { + currency_id: GetCurrencyId::get(), + who: who.clone(), + free_amount: Zero::zero(), + reserved_amount: amount, + }); + } + + fn done_transfer_on_hold( + _reason: &Self::Reason, + source: &T::AccountId, + dest: &T::AccountId, + amount: Self::Balance, + ) { + // TODO: fungibles::MutateHold::transfer_on_hold did not pass the mode to this + // hook, use `BalanceStatus::Reserved` temporarily, need to fix it + Pallet::::deposit_event(Event::::ReserveRepatriated { + currency_id: GetCurrencyId::get(), + from: source.clone(), + to: dest.clone(), + amount, + status: BalanceStatus::Reserved, + }); + } +} + +impl fungible::UnbalancedHold for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn set_balance_on_hold(reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + as fungibles::UnbalancedHold<_>>::set_balance_on_hold(GetCurrencyId::get(), reason, who, amount) + } +} + +impl fungible::BalancedHold for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + fn done_slash(_reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) { + Pallet::::deposit_event(Event::::Slashed { + currency_id: GetCurrencyId::get(), + who: who.clone(), + free_amount: amount, + reserved_amount: Zero::zero(), + }); + } +} diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index 2175b1b6a..10f18f35f 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -38,23 +38,18 @@ #![allow(clippy::unused_unit)] #![allow(clippy::comparison_chain)] -pub use crate::imbalances::{NegativeImbalance, PositiveImbalance}; - use codec::MaxEncodedLen; use frame_support::{ ensure, log, pallet_prelude::*, traits::{ tokens::{ - fungible, fungibles, DepositConsequence, Fortitude, Precision, Preservation, Provenance, Restriction, + fungible, fungibles, DepositConsequence, Fortitude, Precision, Preservation, Provenance, WithdrawConsequence, }, - BalanceStatus as Status, Contains, Currency as PalletCurrency, DefensiveSaturating, ExistenceRequirement, Get, - Imbalance, LockableCurrency as PalletLockableCurrency, - NamedReservableCurrency as PalletNamedReservableCurrency, ReservableCurrency as PalletReservableCurrency, - SignedImbalance, WithdrawReasons, + Contains, DefensiveSaturating, ExistenceRequirement, Get, Imbalance, OnUnbalanced, }, - transactional, BoundedVec, + BoundedVec, }; use frame_system::{ensure_signed, pallet_prelude::*}; use scale_info::TypeInfo; @@ -74,48 +69,24 @@ use orml_traits::{ MultiReservableCurrency, NamedMultiReservableCurrency, }; -mod imbalances; +mod fungibles_conformance_tests; +mod impl_currency; +mod impl_fungibles; mod impls; mod mock; mod tests; mod tests_currency_adapter; mod tests_events; mod tests_fungibles; +mod tests_fungibles_conformance; mod tests_multicurrency; mod weights; +pub use impl_currency::{NegativeImbalance, PositiveImbalance}; pub use impls::*; pub use weights::WeightInfo; -pub struct TransferDust(marker::PhantomData<(T, GetAccountId)>); -impl OnDust for TransferDust -where - T: Config, - GetAccountId: Get, -{ - fn on_dust(who: &T::AccountId, currency_id: T::CurrencyId, amount: T::Balance) { - // transfer the dust to treasury account, ignore the result, - // if failed will leave some dust which still could be recycled. - let _ = Pallet::::do_transfer( - currency_id, - who, - &GetAccountId::get(), - amount, - ExistenceRequirement::AllowDeath, - ); - } -} - -pub struct BurnDust(marker::PhantomData); -impl OnDust for BurnDust { - fn on_dust(who: &T::AccountId, currency_id: T::CurrencyId, amount: T::Balance) { - // burn the dust, ignore the result, - // if failed will leave some dust which still could be recycled. - let _ = Pallet::::do_withdraw(currency_id, who, amount, ExistenceRequirement::AllowDeath, true); - } -} - /// 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, MaxEncodedLen, RuntimeDebug, TypeInfo)] @@ -160,14 +131,9 @@ pub struct AccountData { } impl AccountData { - /// The amount that this account's free balance may not be reduced - /// beyond. - pub(crate) fn frozen(&self) -> Balance { - self.frozen - } /// The total balance in this account including any that is reserved and /// ignoring any frozen. - fn total(&self) -> Balance { + pub(crate) fn total(&self) -> Balance { self.free.saturating_add(self.reserved) } } @@ -176,9 +142,8 @@ pub use module::*; #[frame_support::pallet] pub mod module { - use orml_traits::currency::MutationHooks; - use super::*; + use orml_traits::currency::MutationHooks; #[pallet::config] pub trait Config: frame_system::Config { @@ -236,6 +201,9 @@ pub mod module { // The whitelist of accounts that will not be reaped even if its total // is zero or below ED. type DustRemovalWhitelist: Contains; + + /// Handler for the unbalanced reduction when removing a dust account. + type DustRemoval: OnUnbalanced>; } #[pallet::error] @@ -450,7 +418,7 @@ pub mod module { *initial_balance >= T::ExistentialDeposits::get(currency_id), "the balance of any account should always be more than existential deposit.", ); - Pallet::::mutate_account(account_id, *currency_id, |account_data, _| { + Pallet::::mutate_account(*currency_id, account_id, |account_data| { account_data.free = *initial_balance }); TotalIssuance::::mutate(*currency_id, |total_issuance| { @@ -609,43 +577,52 @@ pub mod module { ensure_root(origin)?; let who = T::Lookup::lookup(who)?; - Self::try_mutate_account(&who, currency_id, |account, _| -> DispatchResult { - let mut new_total = new_free.checked_add(&new_reserved).ok_or(ArithmeticError::Overflow)?; - let (new_free, new_reserved) = if new_total < T::ExistentialDeposits::get(¤cy_id) { - new_total = Zero::zero(); - (Zero::zero(), Zero::zero()) - } else { - (new_free, new_reserved) - }; - let old_total = account.total(); - - account.free = new_free; - account.reserved = new_reserved; - - if new_total > old_total { - TotalIssuance::::try_mutate(currency_id, |t| -> DispatchResult { - *t = t - .checked_add(&(new_total.defensive_saturating_sub(old_total))) - .ok_or(ArithmeticError::Overflow)?; - Ok(()) - })?; - } else if new_total < old_total { - TotalIssuance::::try_mutate(currency_id, |t| -> DispatchResult { - *t = t - .checked_sub(&(old_total.defensive_saturating_sub(new_total))) - .ok_or(ArithmeticError::Underflow)?; - Ok(()) - })?; - } + let mut new_total = new_free.checked_add(&new_reserved).ok_or(ArithmeticError::Overflow)?; + let ed = Self::ed(currency_id); + let wipeout = new_free < ed && new_reserved.is_zero() && !Self::in_dust_removal_whitelist(&who); + let (new_free, new_reserved) = if wipeout { + new_total = Zero::zero(); + (Zero::zero(), Zero::zero()) + } else { + (new_free, new_reserved) + }; - Self::deposit_event(Event::BalanceSet { - currency_id, - who: who.clone(), - free: new_free, - reserved: new_reserved, - }); - Ok(()) - })?; + // First we try to modify the account's balance to the forced balance. + let old_total = Self::try_mutate_account_handling_dust( + currency_id, + &who, + |account, _| -> Result { + let old_total = account.total(); + account.free = new_free; + account.reserved = new_reserved; + Ok(old_total) + }, + )?; + + // This will adjust the total issuance, which was not done by the + // `mutate_account` above. + if new_total > old_total { + TotalIssuance::::try_mutate(currency_id, |t| -> DispatchResult { + *t = t + .checked_add(&(new_total.defensive_saturating_sub(old_total))) + .ok_or(ArithmeticError::Overflow)?; + Ok(()) + })?; + } else if new_total < old_total { + TotalIssuance::::try_mutate(currency_id, |t| -> DispatchResult { + *t = t + .checked_sub(&(old_total.defensive_saturating_sub(new_total))) + .ok_or(ArithmeticError::Underflow)?; + Ok(()) + })?; + } + + Self::deposit_event(Event::BalanceSet { + currency_id, + who, + free: new_free, + reserved: new_reserved, + }); Ok(()) } @@ -653,81 +630,12 @@ pub mod module { } impl Pallet { - pub(crate) fn deposit_consequence( - _who: &T::AccountId, - currency_id: T::CurrencyId, - amount: T::Balance, - account: &AccountData, - ) -> DepositConsequence { - if amount.is_zero() { - return DepositConsequence::Success; - } - - if TotalIssuance::::get(currency_id).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::ExistentialDeposits::get(¤cy_id) { - 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 ed(currency_id: T::CurrencyId) -> T::Balance { + T::ExistentialDeposits::get(¤cy_id) } - pub(crate) fn withdraw_consequence( - who: &T::AccountId, - currency_id: T::CurrencyId, - amount: T::Balance, - account: &AccountData, - ) -> WithdrawConsequence { - if amount.is_zero() { - return WithdrawConsequence::Success; - } - - if TotalIssuance::::get(currency_id).checked_sub(&amount).is_none() { - return WithdrawConsequence::Underflow; - } - - let new_total_balance = match account.total().checked_sub(&amount) { - Some(x) => x, - None => return WithdrawConsequence::BalanceLow, - }; - - // 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::ExistentialDeposits::get(¤cy_id); - 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::BalanceLow, - }; - - // Eventual free funds must be no less than the frozen balance. - if new_free_balance < account.frozen() { - return WithdrawConsequence::Frozen; - } - - success + fn in_dust_removal_whitelist(who: &T::AccountId) -> bool { + T::DustRemovalWhitelist::contains(who) } // Ensure that an account can withdraw from their free balance given any @@ -746,55 +654,81 @@ impl Pallet { .checked_sub(&amount) .ok_or(Error::::BalanceTooLow)?; ensure!( - new_balance >= Self::accounts(who, currency_id).frozen(), + new_balance >= Self::accounts(who, currency_id).frozen, Error::::LiquidityRestrictions ); Ok(()) } + pub(crate) fn wipeout(currency_id: T::CurrencyId, who: &T::AccountId, account: &AccountData) -> bool { + account.free < Self::ed(currency_id) && account.reserved.is_zero() && !Self::in_dust_removal_whitelist(who) + } + + /// 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, currency_id: T::CurrencyId, - f: impl FnOnce(&mut AccountData, bool) -> sp_std::result::Result, - ) -> sp_std::result::Result<(R, Option), E> { + who: &T::AccountId, + f: impl FnOnce(&mut AccountData, bool) -> Result, + ) -> Result<(R, Option), E> { Accounts::::try_mutate_exists(who, currency_id, |maybe_account| { - let existed = maybe_account.is_some(); + let is_new = maybe_account.is_none(); let mut account = maybe_account.take().unwrap_or_default(); - f(&mut account, existed).map(move |result| { - let maybe_endowed = if !existed { Some(account.free) } else { None }; - let mut maybe_dust: Option = None; - let total = account.total(); - *maybe_account = if total < T::ExistentialDeposits::get(¤cy_id) { - // if ED is not zero, but account total is zero, account will be reaped - if total.is_zero() { - None - } else { - if !T::DustRemovalWhitelist::contains(who) { - maybe_dust = Some(total); - } - Some(account) - } + + let result = f(&mut account, is_new)?; + + let maybe_endowed = if is_new { Some(account.free) } else { None }; + + // Handle any steps needed after mutating an account. + // + // Updates `maybe_account` to `Some` iff the account shouldn't be removed. + // 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 or account in DustRemovalWhitelist. + // Reserved being non-zero should imply that we have a consumer ref, so this is economically safe. + let maybe_dust = if Self::wipeout(currency_id, who, &account) { + if account.total().is_zero() { + None } else { - // Note: if ED is zero, account will never be reaped - Some(account) - }; + Some(account.free) + } + } else { + // update account + *maybe_account = Some(account); + None + }; - (maybe_endowed, existed, maybe_account.is_some(), maybe_dust, result) - }) - }) - .map(|(maybe_endowed, existed, exists, maybe_dust, result)| { - if existed && !exists { + let exists = maybe_account.is_some(); + + // NOTE: here differs with pallet-balances, best-effort decrease/increase the provider + // when remove/insert account data. + if !is_new && !exists { // If existed before, decrease account provider. // Ignore the result, because if it failed then there are remaining consumers, // and the account storage in frame_system shouldn't be reaped. let _ = frame_system::Pallet::::dec_providers(who); >::OnKilledTokenAccount::happened(&(who.clone(), currency_id)); - } else if !existed && exists { + } else if is_new && exists { // if new, increase account provider frame_system::Pallet::::inc_providers(who); >::OnNewTokenAccount::happened(&(who.clone(), currency_id)); } + Ok((maybe_endowed, maybe_dust, result)) + }).map(|(maybe_endowed, maybe_dust, result)| { if let Some(endowed) = maybe_endowed { Self::deposit_event(Event::Endowed { currency_id, @@ -804,10 +738,11 @@ impl Pallet { } if let Some(dust_amount) = maybe_dust { - // `OnDust` maybe get/set storage `Accounts` of `who`, trigger handler here - // to avoid some unexpected errors. - >::OnDust::on_dust(who, currency_id, dust_amount); - + >::OnDust::on_dust( + currency_id, + who, + dust_amount, + ); Self::deposit_event(Event::DustLost { currency_id, who: who.clone(), @@ -819,53 +754,74 @@ impl Pallet { }) } + /// 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, currency_id: T::CurrencyId, - f: impl FnOnce(&mut AccountData, bool) -> R, + who: &T::AccountId, + f: impl FnOnce(&mut AccountData) -> R, ) -> (R, Option) { - Self::try_mutate_account(who, currency_id, |account, existed| -> Result { - Ok(f(account, existed)) + Self::try_mutate_account(currency_id, who, |account, _| -> Result { + Ok(f(account)) }) .expect("Error is infallible; qed") } - /// Set free balance of `who` to a new value. + /// Mutate an account to some new value, or delete it entirely with `None`. + /// Will enforce `ExistentialDeposit` law, annulling the account as needed. /// - /// Note: this will not maintain total issuance, and the caller is expected - /// to do it. If it will cause the account to be removed dust, shouldn't use - /// it, because maybe the account that should be reaped to remain due to - /// failed transfer/withdraw dust. - pub(crate) fn set_free_balance(currency_id: T::CurrencyId, who: &T::AccountId, amount: T::Balance) { - Self::mutate_account(who, currency_id, |account, _| { - account.free = amount; - - Self::deposit_event(Event::BalanceSet { - currency_id, - who: who.clone(), - free: account.free, - reserved: account.reserved, - }); - }); + /// 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( + currency_id: T::CurrencyId, + who: &T::AccountId, + f: impl FnOnce(&mut AccountData, bool) -> Result, + ) -> Result { + let (r, maybe_dust) = Self::try_mutate_account(currency_id, who, f)?; + if let Some(dust) = maybe_dust { + >::handle_raw_dust(currency_id, dust); + } + Ok(r) } - /// Set reserved balance of `who` to a new value. + /// Mutate an account to some new value, or delete it entirely with `None`. + /// Will enforce `ExistentialDeposit` law, annulling the account as needed. /// - /// Note: this will not maintain total issuance, and the caller is expected - /// to do it. If it will cause the account to be removed dust, shouldn't use - /// it, because maybe the account that should be reaped to remain due to - /// failed transfer/withdraw dust. - pub(crate) fn set_reserved_balance(currency_id: T::CurrencyId, who: &T::AccountId, amount: T::Balance) { - Self::mutate_account(who, currency_id, |account, _| { - account.reserved = amount; - - Self::deposit_event(Event::BalanceSet { - currency_id, - who: who.clone(), - free: account.free, - reserved: account.reserved, - }); - }); + /// 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( + currency_id: T::CurrencyId, + who: &T::AccountId, + f: impl FnOnce(&mut AccountData) -> R, + ) -> R { + let (r, maybe_dust) = Self::mutate_account(currency_id, who, f); + if let Some(dust) = maybe_dust { + >::handle_raw_dust(currency_id, dust); + } + r } /// Update the account entry for `who` under `currency_id`, given the @@ -879,57 +835,59 @@ impl Pallet { let mut total_frozen_prev = Zero::zero(); let mut total_frozen_after = Zero::zero(); - // update account data - Self::mutate_account(who, currency_id, |account, _| { + // update account data and locks + let (_, maybe_dust) = Self::try_mutate_account(currency_id, who, |account, _| -> DispatchResult { total_frozen_prev = account.frozen; account.frozen = Zero::zero(); for lock in locks.iter() { account.frozen = account.frozen.max(lock.amount); } total_frozen_after = account.frozen; - }); - // update locks - let existed = Locks::::contains_key(who, currency_id); - if locks.is_empty() { - Locks::::remove(who, currency_id); - if existed { - // decrease account ref count when destruct lock - frame_system::Pallet::::dec_consumers(who); - } - } else { - let bounded_locks: BoundedVec, T::MaxLocks> = - locks.to_vec().try_into().map_err(|_| Error::::MaxLocksExceeded)?; - Locks::::insert(who, currency_id, bounded_locks); - if !existed { - // increase account ref count when initialize lock - if frame_system::Pallet::::inc_consumers(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!( - "Warning: Attempt to introduce lock consumer reference, yet no providers. \ - This is unexpected but should be safe." - ); + // update locks + let existed = Locks::::contains_key(who, currency_id); + if locks.is_empty() { + Locks::::remove(who, currency_id); + if existed { + // decrease account ref count when destruct lock + frame_system::Pallet::::dec_consumers(who); + } + } else { + let bounded_locks: BoundedVec, T::MaxLocks> = + locks.to_vec().try_into().map_err(|_| Error::::MaxLocksExceeded)?; + Locks::::insert(who, currency_id, bounded_locks); + if !existed { + // increase account ref count when initialize lock + if frame_system::Pallet::::inc_consumers(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!( + "Warning: Attempt to introduce lock consumer reference, yet no providers. \ + This is unexpected but should be safe." + ); + } } } - } - if total_frozen_prev < total_frozen_after { - let amount = total_frozen_after.saturating_sub(total_frozen_prev); - Self::deposit_event(Event::Locked { - currency_id, - who: who.clone(), - amount, - }); - } else if total_frozen_prev > total_frozen_after { - let amount = total_frozen_prev.saturating_sub(total_frozen_after); - Self::deposit_event(Event::Unlocked { - currency_id, - who: who.clone(), - amount, - }); - } + if total_frozen_prev < total_frozen_after { + let amount = total_frozen_after.saturating_sub(total_frozen_prev); + Self::deposit_event(Event::Locked { + currency_id, + who: who.clone(), + amount, + }); + } else if total_frozen_prev > total_frozen_after { + let amount = total_frozen_prev.saturating_sub(total_frozen_after); + Self::deposit_event(Event::Unlocked { + currency_id, + who: who.clone(), + amount, + }); + } + Ok(()) + })?; + debug_assert!(maybe_dust.is_none(), "Not altering main balance; qed"); Ok(()) } @@ -957,40 +915,25 @@ impl Pallet { to, amount, )?; - Self::try_mutate_account(to, currency_id, |to_account, _existed| -> DispatchResult { - Self::try_mutate_account(from, currency_id, |from_account, _existed| -> DispatchResult { + + Self::try_mutate_account_handling_dust(currency_id, to, |to_account, _| -> DispatchResult { + Self::try_mutate_account_handling_dust(currency_id, from, |from_account, _| -> DispatchResult { + Self::ensure_can_withdraw(currency_id, from, amount)?; + from_account.free = from_account .free .checked_sub(&amount) .ok_or(Error::::BalanceTooLow)?; to_account.free = to_account.free.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; - let ed = T::ExistentialDeposits::get(¤cy_id); - // if the total of `to_account` is below existential deposit, would return an - // error. - // Note: if `to_account` is in `T::DustRemovalWhitelist`, can bypass this check. - ensure!( - to_account.total() >= ed || T::DustRemovalWhitelist::contains(to), - Error::::ExistentialDeposit - ); - - Self::ensure_can_withdraw(currency_id, from, amount)?; + let to_wipeout = Self::wipeout(currency_id, to, to_account); + ensure!(!to_wipeout, Error::::ExistentialDeposit); let allow_death = existence_requirement == ExistenceRequirement::AllowDeath; let allow_death = allow_death && frame_system::Pallet::::can_dec_provider(from); - let would_be_dead = if from_account.total() < ed { - if from_account.total().is_zero() { - true - } else { - // Note: if account is not in `T::DustRemovalWhitelist`, account will eventually - // be reaped due to the dust removal. - !T::DustRemovalWhitelist::contains(from) - } - } else { - false - }; + let keep_alive = !Self::wipeout(currency_id, from, from_account); - ensure!(allow_death || !would_be_dead, Error::::KeepAlive); + ensure!(allow_death || keep_alive, Error::::KeepAlive); Ok(()) })?; Ok(()) @@ -1030,26 +973,13 @@ impl Pallet { return Ok(()); } - Self::try_mutate_account(who, currency_id, |account, _existed| -> DispatchResult { - Self::ensure_can_withdraw(currency_id, who, amount)?; - let previous_total = account.total(); + Self::ensure_can_withdraw(currency_id, who, amount)?; + Self::try_mutate_account_handling_dust(currency_id, who, |account, _| -> DispatchResult { account.free = account.free.defensive_saturating_sub(amount); - let ed = T::ExistentialDeposits::get(¤cy_id); - let would_be_dead = if account.total() < ed { - if account.total().is_zero() { - true - } else { - // Note: if account is not in `T::DustRemovalWhitelist`, account will eventually - // be reaped due to the dust removal. - !T::DustRemovalWhitelist::contains(who) - } - } else { - false - }; - let would_kill = would_be_dead && (previous_total >= ed || !previous_total.is_zero()); + let keep_alive = !Self::wipeout(currency_id, who, account); ensure!( - existence_requirement == ExistenceRequirement::AllowDeath || !would_kill, + existence_requirement == ExistenceRequirement::AllowDeath || keep_alive, Error::::KeepAlive ); @@ -1095,15 +1025,15 @@ impl Pallet { who, amount, )?; - Self::try_mutate_account(who, currency_id, |account, existed| -> DispatchResult { + Self::try_mutate_account_handling_dust(currency_id, who, |account, is_new| -> DispatchResult { if require_existed { - ensure!(existed, Error::::DeadAccount); + ensure!(!is_new, Error::::DeadAccount); } else { let ed = T::ExistentialDeposits::get(¤cy_id); // Note: if who is in dust removal whitelist, allow to deposit the amount that // below ED to it. ensure!( - amount >= ed || existed || T::DustRemovalWhitelist::contains(who), + amount >= ed || !is_new || T::DustRemovalWhitelist::contains(who), Error::::ExistentialDeposit ); } @@ -1115,6 +1045,12 @@ impl Pallet { TotalIssuance::::mutate(currency_id, |v| *v = new_total_issuance); } account.free = account.free.defensive_saturating_add(amount); + + Self::deposit_event(Event::Deposited { + currency_id, + who: who.clone(), + amount, + }); Ok(()) })?; >::PostDeposit::on_deposit( @@ -1122,1459 +1058,82 @@ impl Pallet { who, amount, )?; - Self::deposit_event(Event::Deposited { - currency_id, - who: who.clone(), - amount, - }); Ok(amount) } -} - -impl MultiCurrency for Pallet { - type CurrencyId = T::CurrencyId; - type Balance = T::Balance; - - fn minimum_balance(currency_id: Self::CurrencyId) -> Self::Balance { - T::ExistentialDeposits::get(¤cy_id) - } - - fn total_issuance(currency_id: Self::CurrencyId) -> Self::Balance { - Self::total_issuance(currency_id) - } - - fn total_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { - Self::accounts(who, currency_id).total() - } - - fn free_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { - Self::accounts(who, currency_id).free - } - - fn ensure_can_withdraw(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - Self::ensure_can_withdraw(currency_id, who, amount) - } - - fn transfer( - currency_id: Self::CurrencyId, - from: &T::AccountId, - to: &T::AccountId, - amount: Self::Balance, - ) -> DispatchResult { - // allow death - Self::do_transfer(currency_id, from, to, amount, ExistenceRequirement::AllowDeath) - } - - fn deposit(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - // do not require existing - Self::do_deposit(currency_id, who, amount, false, true)?; - Ok(()) - } - - fn withdraw(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - // allow death - Self::do_withdraw(currency_id, who, amount, ExistenceRequirement::AllowDeath, true) - } - - // Check if `value` amount of free balance can be slashed from `who`. - fn can_slash(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> bool { - if value.is_zero() { - return true; - } - Self::free_balance(currency_id, who) >= value - } - /// Is a no-op if `value` to be slashed is zero. + /// Move the reserved balance of one account into the balance of another, + /// according to `status`. This will respect freezes/locks only if + /// `fortitude` is `Polite`. /// - /// 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(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> Self::Balance { - if amount.is_zero() { - return amount; - } - - >::OnSlash::on_slash( - currency_id, - who, - amount, - ); - let account = Self::accounts(who, currency_id); - let free_slashed_amount = account.free.min(amount); - // Cannot underflow because free_slashed_amount can never be greater than amount - // but just to be defensive here. - let mut remaining_slash = amount.defensive_saturating_sub(free_slashed_amount); - - // slash free balance - if !free_slashed_amount.is_zero() { - // Cannot underflow becuase free_slashed_amount can never be greater than - // account.free but just to be defensive here. - Self::set_free_balance( - currency_id, - who, - account.free.defensive_saturating_sub(free_slashed_amount), - ); - } - - // slash reserved balance - let reserved_slashed_amount = account.reserved.min(remaining_slash); - - if !reserved_slashed_amount.is_zero() { - // Cannot underflow due to above line but just to be defensive here. - remaining_slash = remaining_slash.defensive_saturating_sub(reserved_slashed_amount); - Self::set_reserved_balance( - currency_id, - who, - account.reserved.defensive_saturating_sub(reserved_slashed_amount), - ); - } - - // Cannot underflow because the slashed value cannot be greater than total - // issuance but just to be defensive here. - TotalIssuance::::mutate(currency_id, |v| { - *v = v.defensive_saturating_sub(amount.defensive_saturating_sub(remaining_slash)) - }); - - Self::deposit_event(Event::Slashed { - currency_id, - who: who.clone(), - free_amount: free_slashed_amount, - reserved_amount: reserved_slashed_amount, - }); - remaining_slash - } -} - -impl MultiCurrencyExtended for Pallet { - type Amount = T::Amount; - - fn update_balance(currency_id: Self::CurrencyId, who: &T::AccountId, by_amount: Self::Amount) -> DispatchResult { - if by_amount.is_zero() { - return Ok(()); + /// Is a no-op if the value to be moved is zero. + /// + /// NOTE: returns actual amount of transferred value in `Ok` case. + pub(crate) fn do_transfer_reserved( + currency_id: T::CurrencyId, + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: T::Balance, + precision: Precision, + fortitude: Fortitude, + status: BalanceStatus, + ) -> Result { + if value.is_zero() { + return Ok(Zero::zero()); } - // Ensure this doesn't overflow. There isn't any traits that exposes - // `saturating_abs` so we need to do it manually. - let by_amount_abs = if by_amount == Self::Amount::min_value() { - Self::Amount::max_value() - } else { - by_amount.abs() + let max = >::reducible_total_balance_on_hold(currency_id, slashed, fortitude); + let actual = match precision { + Precision::BestEffort => value.min(max), + Precision::Exact => value, }; - - let by_balance = - TryInto::::try_into(by_amount_abs).map_err(|_| Error::::AmountIntoBalanceFailed)?; - if by_amount.is_positive() { - Self::deposit(currency_id, who, by_balance) - } else { - Self::withdraw(currency_id, who, by_balance).map(|_| ()) + ensure!(actual <= max, TokenError::FundsUnavailable); + if slashed == beneficiary { + return match status { + BalanceStatus::Free => Ok(actual.saturating_sub(>::unreserve( + currency_id, + slashed, + actual, + ))), + BalanceStatus::Reserved => Ok(actual), + }; } - } -} -impl MultiLockableCurrency for Pallet { - type Moment = BlockNumberFor; + let ((_, maybe_dust_1), maybe_dust_2) = Self::try_mutate_account( + currency_id, + beneficiary, + |to_account, _| -> Result<((), Option), DispatchError> { + Self::try_mutate_account(currency_id, slashed, |from_account, _| -> DispatchResult { + match status { + BalanceStatus::Free => { + to_account.free = to_account.free.checked_add(&actual).ok_or(ArithmeticError::Overflow)? + } + BalanceStatus::Reserved => { + to_account.reserved = to_account + .reserved + .checked_add(&actual) + .ok_or(ArithmeticError::Overflow)? + } + } + from_account.reserved.saturating_reduce(actual); + Ok(()) + }) + }, + )?; - // Set a lock on the balance of `who` under `currency_id`. - // Is a no-op if lock amount is zero. - fn set_lock( - lock_id: LockIdentifier, - currency_id: Self::CurrencyId, - who: &T::AccountId, - amount: Self::Balance, - ) -> DispatchResult { - if amount.is_zero() { - return Ok(()); + if let Some(dust) = maybe_dust_1 { + >::handle_raw_dust(currency_id, dust); } - let mut new_lock = Some(BalanceLock { id: lock_id, amount }); - let mut locks = Self::locks(who, currency_id) - .into_iter() - .filter_map(|lock| { - if lock.id == lock_id { - new_lock.take() - } else { - Some(lock) - } - }) - .collect::>(); - if let Some(lock) = new_lock { - locks.push(lock) + if let Some(dust) = maybe_dust_2 { + >::handle_raw_dust(currency_id, dust); } - Self::update_locks(currency_id, who, &locks[..])?; - Self::deposit_event(Event::LockSet { - lock_id, + Self::deposit_event(Event::ReserveRepatriated { currency_id, - who: who.clone(), - amount, + from: slashed.clone(), + to: beneficiary.clone(), + amount: actual, + status, }); - Ok(()) - } - - // Extend a lock on the balance of `who` under `currency_id`. - // Is a no-op if lock amount is zero - fn extend_lock( - lock_id: LockIdentifier, - currency_id: Self::CurrencyId, - who: &T::AccountId, - amount: Self::Balance, - ) -> DispatchResult { - if amount.is_zero() { - return Ok(()); - } - let mut new_lock = Some(BalanceLock { id: lock_id, amount }); - let mut locks = Self::locks(who, currency_id) - .into_iter() - .filter_map(|lock| { - if lock.id == lock_id { - new_lock.take().map(|nl| BalanceLock { - id: lock.id, - amount: lock.amount.max(nl.amount), - }) - } else { - Some(lock) - } - }) - .collect::>(); - if let Some(lock) = new_lock { - locks.push(lock) - } - Self::update_locks(currency_id, who, &locks[..]) - } - - fn remove_lock(lock_id: LockIdentifier, currency_id: Self::CurrencyId, who: &T::AccountId) -> DispatchResult { - let mut locks = Self::locks(who, currency_id); - locks.retain(|lock| lock.id != lock_id); - let locks_vec = locks.to_vec(); - Self::update_locks(currency_id, who, &locks_vec[..])?; - - Self::deposit_event(Event::LockRemoved { - lock_id, - currency_id, - who: who.clone(), - }); - Ok(()) - } -} - -impl MultiReservableCurrency for Pallet { - /// Check if `who` can reserve `value` from their free balance. - /// - /// Always `true` if value to be reserved is zero. - fn can_reserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> bool { - if value.is_zero() { - return true; - } - Self::ensure_can_withdraw(currency_id, who, value).is_ok() - } - - /// Slash from reserved balance, returning any amount that was unable to - /// be slashed. - /// - /// Is a no-op if the value to be slashed is zero. - fn slash_reserved(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> Self::Balance { - if value.is_zero() { - return value; - } - - >::OnSlash::on_slash( - currency_id, - who, - value, - ); - let reserved_balance = Self::reserved_balance(currency_id, who); - let actual = reserved_balance.min(value); - Self::mutate_account(who, currency_id, |account, _| { - // ensured reserved_balance >= actual but just to be defensive here. - account.reserved = reserved_balance.defensive_saturating_sub(actual); - }); - TotalIssuance::::mutate(currency_id, |v| *v = v.defensive_saturating_sub(actual)); - - Self::deposit_event(Event::Slashed { - currency_id, - who: who.clone(), - free_amount: Zero::zero(), - reserved_amount: actual, - }); - value.defensive_saturating_sub(actual) - } - - fn reserved_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { - Self::accounts(who, currency_id).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(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> DispatchResult { - if value.is_zero() { - return Ok(()); - } - Self::ensure_can_withdraw(currency_id, who, value)?; - - Self::mutate_account(who, currency_id, |account, _| { - account.free = account.free.defensive_saturating_sub(value); - account.reserved = account.reserved.defensive_saturating_add(value); - - Self::deposit_event(Event::Reserved { - currency_id, - 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. - fn unreserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> Self::Balance { - if value.is_zero() { - return value; - } - - let (remaining, _) = Self::mutate_account(who, currency_id, |account, _| { - let actual = account.reserved.min(value); - account.reserved = account.reserved.defensive_saturating_sub(actual); - account.free = account.free.defensive_saturating_add(actual); - - Self::deposit_event(Event::Unreserved { - currency_id, - who: who.clone(), - amount: actual, - }); - value.defensive_saturating_sub(actual) - }); - - remaining - } - - /// 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( - currency_id: Self::CurrencyId, - slashed: &T::AccountId, - beneficiary: &T::AccountId, - value: Self::Balance, - status: BalanceStatus, - ) -> sp_std::result::Result { - if value.is_zero() { - return Ok(value); - } - - if slashed == beneficiary { - return match status { - BalanceStatus::Free => Ok(Self::unreserve(currency_id, slashed, value)), - BalanceStatus::Reserved => Ok(value.saturating_sub(Self::reserved_balance(currency_id, slashed))), - }; - } - - let from_account = Self::accounts(slashed, currency_id); - let to_account = Self::accounts(beneficiary, currency_id); - let actual = from_account.reserved.min(value); - match status { - BalanceStatus::Free => { - Self::set_free_balance( - currency_id, - beneficiary, - to_account.free.defensive_saturating_add(actual), - ); - } - BalanceStatus::Reserved => { - Self::set_reserved_balance( - currency_id, - beneficiary, - to_account.reserved.defensive_saturating_add(actual), - ); - } - } - Self::set_reserved_balance( - currency_id, - slashed, - from_account.reserved.defensive_saturating_sub(actual), - ); - - Self::deposit_event(Event::::ReserveRepatriated { - currency_id, - from: slashed.clone(), - to: beneficiary.clone(), - amount: actual, - status, - }); - Ok(value.defensive_saturating_sub(actual)) - } -} - -impl NamedMultiReservableCurrency for Pallet { - type ReserveIdentifier = T::ReserveIdentifier; - - fn reserved_balance_named( - id: &Self::ReserveIdentifier, - currency_id: Self::CurrencyId, - who: &T::AccountId, - ) -> Self::Balance { - let reserves = Self::reserves(who, currency_id); - 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, - currency_id: Self::CurrencyId, - who: &T::AccountId, - value: Self::Balance, - ) -> DispatchResult { - if value.is_zero() { - return Ok(()); - } - - Reserves::::try_mutate(who, currency_id, |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(currency_id, who, value) - }) - } - - /// 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, - currency_id: Self::CurrencyId, - who: &T::AccountId, - value: Self::Balance, - ) -> Self::Balance { - if value.is_zero() { - return Zero::zero(); - } - - Reserves::::mutate_exists(who, currency_id, |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(currency_id, 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`, but just to be defensive - // here. - reserves[index].amount = reserves[index].amount.defensive_saturating_sub(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.defensive_saturating_sub(actual) - } - Err(_) => value, - } - } else { - value - } - }) - } - - /// Slash from reserved balance, returning the 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, - currency_id: Self::CurrencyId, - who: &T::AccountId, - value: Self::Balance, - ) -> Self::Balance { - if value.is_zero() { - return Zero::zero(); - } - - Reserves::::mutate(who, currency_id, |reserves| -> Self::Balance { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - let to_change = cmp::min(reserves[index].amount, value); - - let remain = >::slash_reserved(currency_id, 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` but just to be defensive - // here. - reserves[index].amount = reserves[index].amount.defensive_saturating_sub(actual); - - Self::deposit_event(Event::Slashed { - who: who.clone(), - currency_id, - free_amount: Zero::zero(), - reserved_amount: actual, - }); - value.defensive_saturating_sub(actual) - } - Err(_) => 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, - currency_id: Self::CurrencyId, - 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, currency_id, slashed, value)), - Status::Reserved => Ok(value.saturating_sub(Self::reserved_balance_named(id, currency_id, slashed))), - }; - } - - Reserves::::try_mutate( - slashed, - currency_id, - |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, - currency_id, - |reserves| -> Result { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - let remain = >::repatriate_reserved( - currency_id, - 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( - currency_id, - 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( - currency_id, - 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` but just to be defensive - // here. - reserves[index].amount = reserves[index].amount.defensive_saturating_sub(actual); - Ok(value.defensive_saturating_sub(actual)) - } - Err(_) => Ok(value), - } - }, - ) - } -} - -impl fungibles::Inspect for Pallet { - type AssetId = T::CurrencyId; - type Balance = T::Balance; - - fn total_issuance(asset_id: Self::AssetId) -> Self::Balance { - Self::total_issuance(asset_id) - } - - fn minimum_balance(asset_id: Self::AssetId) -> Self::Balance { - T::ExistentialDeposits::get(&asset_id) - } - - fn balance(asset_id: Self::AssetId, who: &T::AccountId) -> Self::Balance { - Self::accounts(who, asset_id).free - } - - fn total_balance(asset_id: Self::AssetId, who: &T::AccountId) -> Self::Balance { - Self::accounts(who, asset_id).total() - } - - fn reducible_balance( - asset_id: Self::AssetId, - who: &T::AccountId, - preservation: Preservation, - _force: Fortitude, - ) -> Self::Balance { - let a = Self::accounts(who, asset_id); - // Liquid balance is what is neither reserved nor locked/frozen. - let liquid = a.free.saturating_sub(a.frozen); - if frame_system::Pallet::::can_dec_provider(who) && !matches!(preservation, Preservation::Protect) { - 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::ExistentialDeposits::get(&asset_id).saturating_sub(a.total().saturating_sub(liquid)); - liquid.saturating_sub(must_remain_to_exist) - } - } - - fn can_deposit( - asset_id: Self::AssetId, - who: &T::AccountId, - amount: Self::Balance, - _provenance: Provenance, - ) -> DepositConsequence { - Self::deposit_consequence(who, asset_id, amount, &Self::accounts(who, asset_id)) - } - - fn can_withdraw( - asset_id: Self::AssetId, - who: &T::AccountId, - amount: Self::Balance, - ) -> WithdrawConsequence { - Self::withdraw_consequence(who, asset_id, amount, &Self::accounts(who, asset_id)) - } - - fn asset_exists(asset: Self::AssetId) -> bool { - TotalIssuance::::contains_key(asset) - } -} - -impl fungibles::Mutate for Pallet { - fn mint_into( - asset_id: Self::AssetId, - who: &T::AccountId, - amount: Self::Balance, - ) -> Result { - Self::deposit_consequence(who, asset_id, amount, &Self::accounts(who, asset_id)).into_result()?; - // do not require existing - Self::do_deposit(asset_id, who, amount, false, true) - } - - fn burn_from( - asset_id: Self::AssetId, - who: &T::AccountId, - amount: Self::Balance, - // TODO: Respect precision - _precision: Precision, - // TODO: Respect fortitude - _fortitude: Fortitude, - ) -> Result { - let extra = - Self::withdraw_consequence(who, asset_id, amount, &Self::accounts(who, asset_id)).into_result(false)?; - let actual = amount.defensive_saturating_add(extra); - // allow death - Self::do_withdraw(asset_id, who, actual, ExistenceRequirement::AllowDeath, true).map(|_| actual) - } - - fn transfer( - asset_id: Self::AssetId, - source: &T::AccountId, - dest: &T::AccountId, - amount: T::Balance, - preservation: Preservation, - ) -> Result { - let existence_requirement = match preservation { - Preservation::Expendable => ExistenceRequirement::AllowDeath, - Preservation::Protect | Preservation::Preserve => ExistenceRequirement::KeepAlive, - }; - Self::do_transfer(asset_id, source, dest, amount, existence_requirement).map(|_| amount) - } -} - -impl fungibles::Unbalanced for Pallet { - fn handle_dust(_dust: fungibles::Dust) { - // Dust is handled in account mutate method - } - - fn write_balance( - asset_id: Self::AssetId, - who: &T::AccountId, - amount: Self::Balance, - ) -> Result, DispatchError> { - let max_reduction = >::reducible_balance( - asset_id, - who, - Preservation::Expendable, - Fortitude::Force, - ); - - // Balance is the same type and will not overflow - let (_, dust_amount) = Self::try_mutate_account(who, asset_id, |account, _| -> Result<(), DispatchError> { - // 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::::BalanceTooLow); - - account.free = amount; - Self::deposit_event(Event::BalanceSet { - currency_id: asset_id, - who: who.clone(), - free: account.free, - reserved: account.reserved, - }); - - Ok(()) - })?; - - Ok(dust_amount) - } - - fn set_total_issuance(asset_id: Self::AssetId, amount: Self::Balance) { - // Balance is the same type and will not overflow - TotalIssuance::::mutate(asset_id, |t| *t = amount); - - Self::deposit_event(Event::TotalIssuanceSet { - currency_id: asset_id, - amount, - }); - } - - fn decrease_balance( - asset: Self::AssetId, - who: &T::AccountId, - mut amount: Self::Balance, - precision: Precision, - preservation: Preservation, - force: Fortitude, - ) -> Result { - let old_balance = as fungibles::Inspect>::balance(asset, who); - let free = as fungibles::Inspect>::reducible_balance(asset, who, preservation, force); - if let Precision::BestEffort = precision { - amount = amount.min(free); - } - let new_balance = old_balance.checked_sub(&amount).ok_or(TokenError::FundsUnavailable)?; - let _dust_amount = Self::write_balance(asset, who, new_balance)?.unwrap_or_default(); - - // here just return decrease amount, shouldn't count the dust_amount - Ok(old_balance.saturating_sub(new_balance)) - } -} - -impl fungibles::Balanced for Pallet { - type OnDropDebt = fungibles::IncreaseIssuance; - type OnDropCredit = fungibles::DecreaseIssuance; - - fn done_deposit(currency_id: Self::AssetId, who: &T::AccountId, amount: Self::Balance) { - Self::deposit_event(Event::Deposited { - currency_id, - who: who.clone(), - amount, - }); - } - fn done_withdraw(currency_id: Self::AssetId, who: &T::AccountId, amount: Self::Balance) { - Self::deposit_event(Event::Withdrawn { - currency_id, - who: who.clone(), - amount, - }); - } - fn done_issue(currency_id: Self::AssetId, amount: Self::Balance) { - Self::deposit_event(Event::Issued { currency_id, amount }); - } - fn done_rescind(currency_id: Self::AssetId, amount: Self::Balance) { - Self::deposit_event(Event::Rescinded { currency_id, amount }); - } -} - -type ReasonOf =

::AccountId>>::Reason; -impl fungibles::InspectHold for Pallet { - type Reason = (); - - fn balance_on_hold(asset_id: Self::AssetId, _reason: &Self::Reason, who: &T::AccountId) -> T::Balance { - Self::accounts(who, asset_id).reserved - } - - fn total_balance_on_hold(asset: Self::AssetId, who: &T::AccountId) -> Self::Balance { - Self::accounts(who, asset).reserved - } - - fn reducible_total_balance_on_hold(_asset: Self::AssetId, _who: &T::AccountId, _force: Fortitude) -> Self::Balance { - 0u32.into() - } - - fn hold_available(_asset: Self::AssetId, _reason: &Self::Reason, _who: &T::AccountId) -> bool { - true - } - - fn can_hold(asset_id: Self::AssetId, _reason: &Self::Reason, who: &T::AccountId, amount: T::Balance) -> bool { - let a = Self::accounts(who, asset_id); - let min_balance = T::ExistentialDeposits::get(&asset_id).max(a.frozen); - 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 fungibles::MutateHold for Pallet { - fn hold( - asset_id: Self::AssetId, - _reason: &ReasonOf, - who: &T::AccountId, - amount: Self::Balance, - ) -> DispatchResult { - as MultiReservableCurrency<_>>::reserve(asset_id, who, amount) - } - - fn release( - asset_id: Self::AssetId, - _reason: &ReasonOf, - who: &T::AccountId, - amount: Self::Balance, - precision: Precision, - ) -> Result { - if amount.is_zero() { - return Ok(amount); - } - - // Done on a best-effort basis. - let (released, _) = - Self::try_mutate_account(who, asset_id, |a, _existed| -> Result { - let new_free = a.free.saturating_add(amount.min(a.reserved)); - let actual = new_free.defensive_saturating_sub(a.free); - // Guaranteed to be <= amount and <= a.reserved - ensure!( - matches!(precision, Precision::BestEffort) || actual == amount, - Error::::BalanceTooLow - ); - a.free = new_free; - a.reserved = a.reserved.saturating_sub(actual); - - Self::deposit_event(Event::Unreserved { - currency_id: asset_id, - who: who.clone(), - amount, - }); - Ok(actual) - })?; - - Ok(released) - } - - fn transfer_on_hold( - asset_id: Self::AssetId, - reason: &ReasonOf, - source: &T::AccountId, - dest: &T::AccountId, - amount: Self::Balance, - precision: Precision, - restriction: Restriction, - _fortitude: Fortitude, - ) -> Result { - let status = if restriction == Restriction::OnHold { - Status::Reserved - } else { - Status::Free - }; - ensure!( - amount <= >::balance_on_hold(asset_id, reason, source) - || precision == Precision::BestEffort, - Error::::BalanceTooLow - ); - let gap = Self::repatriate_reserved(asset_id, source, dest, amount, status)?; - // return actual transferred amount - Ok(amount.saturating_sub(gap)) - } -} - -impl fungibles::UnbalancedHold for Pallet { - fn set_balance_on_hold( - asset: Self::AssetId, - _reason: &Self::Reason, - who: &T::AccountId, - amount: Self::Balance, - ) -> DispatchResult { - // Balance is the same type and will not overflow - Self::try_mutate_account(who, asset, |account, _| -> Result<(), DispatchError> { - let old_reserved = account.reserved; - account.reserved = amount; - // free = free + old - new - account.free = account - .free - .checked_add(&old_reserved) - .ok_or(ArithmeticError::Overflow)? - .checked_sub(&account.reserved) - .ok_or(TokenError::BelowMinimum)?; - - Self::deposit_event(Event::BalanceSet { - currency_id: asset, - who: who.clone(), - free: account.free, - reserved: account.reserved, - }); - - Ok(()) - }) - .map(|_| ()) - } -} - -pub struct CurrencyAdapter(marker::PhantomData<(T, GetCurrencyId)>); - -impl PalletCurrency for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - type Balance = T::Balance; - type PositiveImbalance = PositiveImbalance; - type NegativeImbalance = NegativeImbalance; - - fn total_balance(who: &T::AccountId) -> Self::Balance { - as MultiCurrency<_>>::total_balance(GetCurrencyId::get(), who) - } - - fn can_slash(who: &T::AccountId, value: Self::Balance) -> bool { - as MultiCurrency<_>>::can_slash(GetCurrencyId::get(), who, value) - } - - fn total_issuance() -> Self::Balance { - as MultiCurrency<_>>::total_issuance(GetCurrencyId::get()) - } - - fn minimum_balance() -> Self::Balance { - as MultiCurrency<_>>::minimum_balance(GetCurrencyId::get()) - } - - fn burn(mut amount: Self::Balance) -> Self::PositiveImbalance { - if amount.is_zero() { - return PositiveImbalance::zero(); - } - let currency_id = GetCurrencyId::get(); - TotalIssuance::::mutate(currency_id, |issued| { - *issued = issued.checked_sub(&amount).unwrap_or_else(|| { - amount = *issued; - Zero::zero() - }) - }); - - Pallet::::deposit_event(Event::TotalIssuanceSet { - currency_id, - amount: Self::total_issuance(), - }); - PositiveImbalance::new(amount) - } - - fn issue(mut amount: Self::Balance) -> Self::NegativeImbalance { - if amount.is_zero() { - return NegativeImbalance::zero(); - } - TotalIssuance::::mutate(GetCurrencyId::get(), |issued| { - *issued = issued.checked_add(&amount).unwrap_or_else(|| { - amount = Self::Balance::max_value().defensive_saturating_sub(*issued); - Self::Balance::max_value() - }) - }); - - Pallet::::deposit_event(Event::TotalIssuanceSet { - currency_id: GetCurrencyId::get(), - amount: Self::total_issuance(), - }); - NegativeImbalance::new(amount) - } - - fn free_balance(who: &T::AccountId) -> Self::Balance { - as MultiCurrency<_>>::free_balance(GetCurrencyId::get(), who) - } - - fn ensure_can_withdraw( - who: &T::AccountId, - amount: Self::Balance, - _reasons: WithdrawReasons, - _new_balance: Self::Balance, - ) -> DispatchResult { - as MultiCurrency<_>>::ensure_can_withdraw(GetCurrencyId::get(), who, amount) - } - - fn transfer( - source: &T::AccountId, - dest: &T::AccountId, - value: Self::Balance, - existence_requirement: ExistenceRequirement, - ) -> DispatchResult { - Pallet::::do_transfer(GetCurrencyId::get(), source, dest, value, existence_requirement) - } - - fn slash(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { - if value.is_zero() { - return (Self::NegativeImbalance::zero(), value); - } - - let currency_id = GetCurrencyId::get(); - let account = Pallet::::accounts(who, currency_id); - let free_slashed_amount = account.free.min(value); - let mut remaining_slash = value.defensive_saturating_sub(free_slashed_amount); - - // slash free balance - if !free_slashed_amount.is_zero() { - Pallet::::set_free_balance( - currency_id, - who, - account.free.defensive_saturating_sub(free_slashed_amount), - ); - } - - // slash reserved balance - if !remaining_slash.is_zero() { - let reserved_slashed_amount = account.reserved.min(remaining_slash); - remaining_slash = remaining_slash.defensive_saturating_sub(reserved_slashed_amount); - Pallet::::set_reserved_balance( - currency_id, - who, - account.reserved.defensive_saturating_sub(reserved_slashed_amount), - ); - - Pallet::::deposit_event(Event::Slashed { - currency_id, - who: who.clone(), - free_amount: free_slashed_amount, - reserved_amount: reserved_slashed_amount, - }); - ( - Self::NegativeImbalance::new(free_slashed_amount.saturating_add(reserved_slashed_amount)), - remaining_slash, - ) - } else { - Pallet::::deposit_event(Event::Slashed { - currency_id, - who: who.clone(), - free_amount: value, - reserved_amount: Zero::zero(), - }); - (Self::NegativeImbalance::new(value), remaining_slash) - } - } - - /// Deposit some `value` into the free balance of an existing target account - /// `who`. - fn deposit_into_existing( - who: &T::AccountId, - value: Self::Balance, - ) -> sp_std::result::Result { - // do not change total issuance - Pallet::::do_deposit(GetCurrencyId::get(), who, value, true, false).map(|_| PositiveImbalance::new(value)) - } - - /// Deposit some `value` into the free balance of `who`, possibly creating a - /// new account. - fn deposit_creating(who: &T::AccountId, value: Self::Balance) -> Self::PositiveImbalance { - // do not change total issuance - Pallet::::do_deposit(GetCurrencyId::get(), who, value, false, false) - .map_or_else(|_| Self::PositiveImbalance::zero(), |_| PositiveImbalance::new(value)) - } - - fn withdraw( - who: &T::AccountId, - value: Self::Balance, - _reasons: WithdrawReasons, - liveness: ExistenceRequirement, - ) -> sp_std::result::Result { - // do not change total issuance - Pallet::::do_withdraw(GetCurrencyId::get(), who, value, liveness, false) - .map(|_| Self::NegativeImbalance::new(value)) - } - - fn make_free_balance_be( - who: &T::AccountId, - value: Self::Balance, - ) -> SignedImbalance { - let currency_id = GetCurrencyId::get(); - Pallet::::try_mutate_account( - who, - currency_id, - |account, existed| -> Result, ()> { - // 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. - let ed = T::ExistentialDeposits::get(¤cy_id); - ensure!(value.saturating_add(account.reserved) >= ed || existed, ()); - - let imbalance = if account.free <= value { - SignedImbalance::Positive(PositiveImbalance::new(value.saturating_sub(account.free))) - } else { - SignedImbalance::Negative(NegativeImbalance::new(account.free.saturating_sub(value))) - }; - account.free = value; - - Pallet::::deposit_event(Event::BalanceSet { - currency_id, - who: who.clone(), - free: value, - reserved: account.reserved, - }); - Ok(imbalance) - }, - ) - .map(|(imbalance, _)| imbalance) - .unwrap_or_else(|_| SignedImbalance::Positive(Self::PositiveImbalance::zero())) - } -} - -impl PalletReservableCurrency for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - fn can_reserve(who: &T::AccountId, value: Self::Balance) -> bool { - as MultiReservableCurrency<_>>::can_reserve(GetCurrencyId::get(), who, value) - } - - fn slash_reserved(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { - let actual = as MultiReservableCurrency<_>>::slash_reserved(GetCurrencyId::get(), who, value); - (Self::NegativeImbalance::zero(), actual) - } - - fn reserved_balance(who: &T::AccountId) -> Self::Balance { - as MultiReservableCurrency<_>>::reserved_balance(GetCurrencyId::get(), who) - } - - fn reserve(who: &T::AccountId, value: Self::Balance) -> DispatchResult { - as MultiReservableCurrency<_>>::reserve(GetCurrencyId::get(), who, value) - } - - fn unreserve(who: &T::AccountId, value: Self::Balance) -> Self::Balance { - as MultiReservableCurrency<_>>::unreserve(GetCurrencyId::get(), who, value) - } - - fn repatriate_reserved( - slashed: &T::AccountId, - beneficiary: &T::AccountId, - value: Self::Balance, - status: Status, - ) -> sp_std::result::Result { - as MultiReservableCurrency<_>>::repatriate_reserved( - GetCurrencyId::get(), - slashed, - beneficiary, - value, - status, - ) - } -} - -impl PalletNamedReservableCurrency for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - type ReserveIdentifier = T::ReserveIdentifier; - - fn reserved_balance_named(id: &Self::ReserveIdentifier, who: &T::AccountId) -> Self::Balance { - as NamedMultiReservableCurrency<_>>::reserved_balance_named(id, GetCurrencyId::get(), who) - } - - fn reserve_named(id: &Self::ReserveIdentifier, who: &T::AccountId, value: Self::Balance) -> DispatchResult { - as NamedMultiReservableCurrency<_>>::reserve_named(id, GetCurrencyId::get(), who, value) - } - - fn unreserve_named(id: &Self::ReserveIdentifier, who: &T::AccountId, value: Self::Balance) -> Self::Balance { - as NamedMultiReservableCurrency<_>>::unreserve_named(id, GetCurrencyId::get(), who, value) - } - - fn slash_reserved_named( - id: &Self::ReserveIdentifier, - who: &T::AccountId, - value: Self::Balance, - ) -> (Self::NegativeImbalance, Self::Balance) { - let actual = - as NamedMultiReservableCurrency<_>>::slash_reserved_named(id, GetCurrencyId::get(), who, value); - (Self::NegativeImbalance::zero(), actual) - } - - fn repatriate_reserved_named( - id: &Self::ReserveIdentifier, - slashed: &T::AccountId, - beneficiary: &T::AccountId, - value: Self::Balance, - status: Status, - ) -> sp_std::result::Result { - as NamedMultiReservableCurrency<_>>::repatriate_reserved_named( - id, - GetCurrencyId::get(), - slashed, - beneficiary, - value, - status, - ) - } -} - -impl PalletLockableCurrency for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - type Moment = BlockNumberFor; - type MaxLocks = (); - - fn set_lock(id: LockIdentifier, who: &T::AccountId, amount: Self::Balance, _reasons: WithdrawReasons) { - let _ = as MultiLockableCurrency<_>>::set_lock(id, GetCurrencyId::get(), who, amount); - } - - fn extend_lock(id: LockIdentifier, who: &T::AccountId, amount: Self::Balance, _reasons: WithdrawReasons) { - let _ = as MultiLockableCurrency<_>>::extend_lock(id, GetCurrencyId::get(), who, amount); - } - - fn remove_lock(id: LockIdentifier, who: &T::AccountId) { - let _ = as MultiLockableCurrency<_>>::remove_lock(id, GetCurrencyId::get(), who); - } -} - -impl TransferAll for Pallet { - #[transactional] - fn transfer_all(source: &T::AccountId, dest: &T::AccountId) -> DispatchResult { - Accounts::::iter_prefix(source).try_for_each(|(currency_id, account_data)| -> DispatchResult { - // allow death - Self::do_transfer( - currency_id, - source, - dest, - account_data.free, - ExistenceRequirement::AllowDeath, - ) - }) - } -} - -impl fungible::Inspect for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - type Balance = T::Balance; - - fn total_issuance() -> Self::Balance { - as fungibles::Inspect<_>>::total_issuance(GetCurrencyId::get()) - } - fn minimum_balance() -> Self::Balance { - as fungibles::Inspect<_>>::minimum_balance(GetCurrencyId::get()) - } - fn balance(who: &T::AccountId) -> Self::Balance { - as fungibles::Inspect<_>>::balance(GetCurrencyId::get(), who) - } - fn total_balance(who: &T::AccountId) -> Self::Balance { - as fungibles::Inspect<_>>::total_balance(GetCurrencyId::get(), who) - } - fn reducible_balance(who: &T::AccountId, preservation: Preservation, fortitude: Fortitude) -> Self::Balance { - as fungibles::Inspect<_>>::reducible_balance(GetCurrencyId::get(), who, preservation, fortitude) - } - fn can_deposit(who: &T::AccountId, amount: Self::Balance, provenance: Provenance) -> DepositConsequence { - as fungibles::Inspect<_>>::can_deposit(GetCurrencyId::get(), who, amount, provenance) - } - fn can_withdraw(who: &T::AccountId, amount: Self::Balance) -> WithdrawConsequence { - as fungibles::Inspect<_>>::can_withdraw(GetCurrencyId::get(), who, amount) - } -} - -impl fungible::Mutate for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - fn mint_into(who: &T::AccountId, amount: Self::Balance) -> Result { - as fungibles::Mutate<_>>::mint_into(GetCurrencyId::get(), who, amount) - } - fn burn_from( - who: &T::AccountId, - amount: Self::Balance, - precision: Precision, - fortitude: Fortitude, - ) -> Result { - as fungibles::Mutate<_>>::burn_from(GetCurrencyId::get(), who, amount, precision, fortitude) - } - - fn transfer( - source: &T::AccountId, - dest: &T::AccountId, - amount: T::Balance, - preservation: Preservation, - ) -> Result { - as fungibles::Mutate<_>>::transfer(GetCurrencyId::get(), source, dest, amount, preservation) - } -} - -impl fungible::Unbalanced for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - fn handle_dust(_dust: fungible::Dust) { - // Dust is handled in account mutate method - } - - fn write_balance(who: &T::AccountId, amount: Self::Balance) -> Result, DispatchError> { - as fungibles::Unbalanced<_>>::write_balance(GetCurrencyId::get(), who, amount) - } - fn set_total_issuance(amount: Self::Balance) { - as fungibles::Unbalanced<_>>::set_total_issuance(GetCurrencyId::get(), amount) - } -} - -type ReasonOfFungible =

::AccountId>>::Reason; -impl fungible::InspectHold for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - type Reason = as fungibles::InspectHold>::Reason; - - fn balance_on_hold(reason: &Self::Reason, who: &T::AccountId) -> T::Balance { - as fungibles::InspectHold<_>>::balance_on_hold(GetCurrencyId::get(), reason, who) - } - fn total_balance_on_hold(who: &T::AccountId) -> Self::Balance { - as fungibles::InspectHold<_>>::total_balance_on_hold(GetCurrencyId::get(), who) - } - fn reducible_total_balance_on_hold(who: &T::AccountId, force: Fortitude) -> Self::Balance { - as fungibles::InspectHold<_>>::reducible_total_balance_on_hold(GetCurrencyId::get(), who, force) - } - fn hold_available(reason: &Self::Reason, who: &T::AccountId) -> bool { - as fungibles::InspectHold<_>>::hold_available(GetCurrencyId::get(), reason, who) - } - fn can_hold(reason: &Self::Reason, who: &T::AccountId, amount: T::Balance) -> bool { - as fungibles::InspectHold<_>>::can_hold(GetCurrencyId::get(), reason, who, amount) - } -} - -impl fungible::MutateHold for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - fn hold(reason: &ReasonOfFungible, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - as fungibles::MutateHold<_>>::hold(GetCurrencyId::get(), reason, who, amount) - } - fn release( - reason: &ReasonOfFungible, - who: &T::AccountId, - amount: Self::Balance, - precision: Precision, - ) -> Result { - as fungibles::MutateHold<_>>::release(GetCurrencyId::get(), reason, who, amount, precision) - } - fn transfer_on_hold( - reason: &ReasonOfFungible, - source: &T::AccountId, - dest: &T::AccountId, - amount: Self::Balance, - precision: Precision, - restriction: Restriction, - fortitude: Fortitude, - ) -> Result { - as fungibles::MutateHold<_>>::transfer_on_hold( - GetCurrencyId::get(), - reason, - source, - dest, - amount, - precision, - restriction, - fortitude, - ) - } -} - -impl fungible::UnbalancedHold for CurrencyAdapter -where - T: Config, - GetCurrencyId: Get, -{ - fn set_balance_on_hold(reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - as fungibles::UnbalancedHold<_>>::set_balance_on_hold(GetCurrencyId::get(), reason, who, amount) + Ok(actual) } } diff --git a/tokens/src/mock.rs b/tokens/src/mock.rs index cd8785d4c..b261bc4ba 100644 --- a/tokens/src/mock.rs +++ b/tokens/src/mock.rs @@ -28,7 +28,8 @@ pub const ALICE: AccountId = AccountId32::new([0u8; 32]); pub const BOB: AccountId = AccountId32::new([1u8; 32]); pub const CHARLIE: AccountId = AccountId32::new([2u8; 32]); pub const DAVE: AccountId = AccountId32::new([3u8; 32]); -pub const TREASURY_ACCOUNT: AccountId = AccountId32::new([4u8; 32]); +pub const EVE: AccountId = AccountId32::new([4u8; 32]); +pub const TREASURY_ACCOUNT: AccountId = AccountId32::new([5u8; 32]); pub const ID_1: LockIdentifier = *b"1 "; pub const ID_2: LockIdentifier = *b"2 "; pub const ID_3: LockIdentifier = *b"3 "; @@ -204,7 +205,7 @@ impl pallet_elections_phragmen::Config for Runtime { pub struct MockDustRemovalWhitelist; impl Contains for MockDustRemovalWhitelist { fn contains(a: &AccountId) -> bool { - *a == DAVE || *a == DustReceiver::get() + *a == DAVE || *a == DustReceiverAccount::get() } } @@ -289,6 +290,7 @@ where } thread_local! { + pub static ON_DUST_CALLS: RefCell = RefCell::new(0); pub static ON_SLASH_CALLS: RefCell = RefCell::new(0); pub static ON_DEPOSIT_PREHOOK_CALLS: RefCell = RefCell::new(0); pub static ON_DEPOSIT_POSTHOOK_CALLS: RefCell = RefCell::new(0); @@ -296,6 +298,18 @@ thread_local! { pub static ON_TRANSFER_POSTHOOK_CALLS: RefCell = RefCell::new(0); } +pub struct OnDustHook(marker::PhantomData); +impl OnDust for OnDustHook { + fn on_dust(_currency_id: T::CurrencyId, _account_id: &T::AccountId, _amount: T::Balance) { + ON_DUST_CALLS.with(|cell| *cell.borrow_mut() += 1); + } +} +impl OnDustHook { + pub fn calls() -> u32 { + ON_SLASH_CALLS.with(|accounts| *accounts.borrow()) + } +} + pub struct OnSlashHook(marker::PhantomData); impl OnSlash for OnSlashHook { fn on_slash(_currency_id: T::CurrencyId, _account_id: &T::AccountId, _amount: T::Balance) { @@ -382,17 +396,13 @@ impl PostTransfer { } } -parameter_types! { - pub DustReceiver: AccountId = PalletId(*b"orml/dst").into_account_truncating(); -} - pub struct CurrencyHooks(marker::PhantomData); impl MutationHooks for CurrencyHooks where T::AccountId: From + Into, T::CurrencyId: From + Into, { - type OnDust = TransferDust; + type OnDust = OnDustHook; type OnSlash = OnSlashHook; type PreDeposit = PreDeposit; type PostDeposit = PostDeposit; @@ -402,6 +412,11 @@ where type OnKilledTokenAccount = TrackKilledAccounts; } +parameter_types! { + pub DustReceiverAccount: AccountId = PalletId(*b"orml/dst").into_account_truncating(); + pub static GetDustReceiverAccount: Option = Some(DustReceiverAccount::get()); +} + impl Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = Balance; @@ -414,6 +429,7 @@ impl Config for Runtime { type MaxReserves = ConstU32<2>; type ReserveIdentifier = ReserveIdentifier; type DustRemovalWhitelist = MockDustRemovalWhitelist; + type DustRemoval = DustReceiver; } pub type TreasuryCurrencyAdapter = ::Currency; diff --git a/tokens/src/tests.rs b/tokens/src/tests.rs index 504ca06ab..e62411df6 100644 --- a/tokens/src/tests.rs +++ b/tokens/src/tests.rs @@ -20,7 +20,7 @@ fn genesis_issuance_should_work() { .execute_with(|| { assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); assert_eq!(Tokens::free_balance(DOT, &BOB), 100); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 0); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 0); assert_eq!(Tokens::total_issuance(DOT), 200); }); } @@ -264,215 +264,432 @@ fn set_balance_should_work() { }); } -// ************************************************* -// tests for inline impl -// ************************************************* +// // ************************************************* +// // tests for utils function +// // ************************************************* #[test] -fn deposit_consequence_should_work() { +fn wipeout_work() { ExtBuilder::default().build().execute_with(|| { assert_eq!( - Tokens::deposit_consequence( - &CHARLIE, + Tokens::wipeout( DOT, - 0, + &ALICE, &AccountData { - free: 1, + free: 0, reserved: 0, frozen: 0 } - ) - .into_result(), - Ok(()) + ), + true ); - - // total issuance overflow assert_eq!( - Tokens::deposit_consequence( - &CHARLIE, - DOT, - Balance::max_value(), + Tokens::wipeout( + ETH, + &ALICE, &AccountData { - free: 1, + free: 0, reserved: 0, frozen: 0 } - ) - .into_result(), - Err(ArithmeticError::Overflow.into()) + ), + false ); - - // total balance overflow assert_eq!( - Tokens::deposit_consequence( - &CHARLIE, + Tokens::wipeout( DOT, - 1, + &DAVE, &AccountData { - free: Balance::max_value(), + free: 0, reserved: 0, frozen: 0 } - ) - .into_result(), - Err(ArithmeticError::Overflow.into()) + ), + false ); - - // below ed assert_eq!( - Tokens::deposit_consequence( - &CHARLIE, - DOT, - 1, + Tokens::wipeout( + ETH, + &DAVE, &AccountData { free: 0, reserved: 0, frozen: 0 } - ) - .into_result(), - Err(TokenError::BelowMinimum.into()) + ), + false ); assert_eq!( - Tokens::deposit_consequence( - &CHARLIE, + Tokens::wipeout( DOT, - 1, + &ALICE, &AccountData { - free: 1, - reserved: 0, + free: 0, + reserved: 1, frozen: 0 } - ) - .into_result(), - Ok(()) + ), + false ); - }); -} - -#[test] -fn withdraw_consequence_should_work() { - ExtBuilder::default().build().execute_with(|| { assert_eq!( - Tokens::withdraw_consequence( + Tokens::wipeout( + ETH, &ALICE, + &AccountData { + free: 0, + reserved: 1, + frozen: 0 + } + ), + false + ); + assert_eq!( + Tokens::wipeout( DOT, - 0, + &DAVE, &AccountData { - free: 1, - reserved: 0, + free: 0, + reserved: 1, frozen: 0 } - ) - .into_result(true), - Ok(Zero::zero()) + ), + false + ); + assert_eq!( + Tokens::wipeout( + ETH, + &DAVE, + &AccountData { + free: 0, + reserved: 1, + frozen: 0 + } + ), + false ); - // total issuance underflow - assert_ok!(Tokens::update_balance(DOT, &ALICE, 2)); - assert_eq!(Tokens::total_issuance(DOT), 2); assert_eq!( - Tokens::withdraw_consequence( - &ALICE, + Tokens::wipeout( DOT, - 3, + &ALICE, &AccountData { free: 1, reserved: 0, frozen: 0 } - ) - .into_result(true), - Err(ArithmeticError::Underflow.into()) + ), + true ); - - // total issuance is not enough assert_eq!( - Tokens::withdraw_consequence( + Tokens::wipeout( + ETH, &ALICE, - DOT, - 2, &AccountData { free: 1, reserved: 0, frozen: 0 } - ) - .into_result(true), - Err(TokenError::FundsUnavailable.into()) + ), + false ); - - // below ED and cannot dec provider - assert_ok!(Tokens::update_balance(DOT, &ALICE, 2)); - assert_eq!(System::providers(&ALICE), 1); - assert_ok!(System::inc_consumers(&ALICE)); - assert!(!System::can_dec_provider(&ALICE)); assert_eq!( - Tokens::withdraw_consequence( - &ALICE, + Tokens::wipeout( DOT, - 1, + &DAVE, &AccountData { - free: 2, + free: 1, reserved: 0, frozen: 0 } - ) - .into_result(true), - Err(TokenError::OnlyProvider.into()) + ), + false ); - - // below ED and can dec provider - let _ = System::inc_providers(&ALICE); - assert!(System::can_dec_provider(&ALICE)); assert_eq!( - Tokens::withdraw_consequence( - &ALICE, - DOT, - 1, + Tokens::wipeout( + ETH, + &DAVE, &AccountData { - free: 2, + free: 1, reserved: 0, frozen: 0 } - ) - .into_result(false), - Ok(1) + ), + false ); + }); +} - // free balance is not enough - assert_eq!( - Tokens::withdraw_consequence( +#[test] +fn try_mutate_account_work() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100), (EVE, DOT, 100)]) + .build() + .execute_with(|| { + // mutate existed account, will not trigger Endowed event + assert_eq!(System::providers(&ALICE), 1); + assert_eq!(Accounts::::contains_key(ALICE, DOT), true); + assert_eq!( + Tokens::accounts(&ALICE, DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 0 + } + ); + assert_ok!(Tokens::try_mutate_account( + DOT, &ALICE, + |account, _| -> DispatchResult { + account.free = 50; + Ok(()) + } + )); + assert_eq!(System::providers(&ALICE), 1); + assert_eq!(Accounts::::contains_key(ALICE, DOT), true); + assert_eq!( + Tokens::accounts(&ALICE, DOT), + AccountData { + free: 50, + reserved: 0, + frozen: 0 + } + ); + assert!(System::events().iter().all(|record| !matches!( + record.event, + RuntimeEvent::Tokens(crate::Event::Endowed { + currency_id: DOT, + who: ALICE, + amount: _ + }) + ))); + + // wipe out account has dust, will trigger DustLost event + assert_ok!(Tokens::try_mutate_account( DOT, - 2, - &AccountData { + &ALICE, + |account, _| -> DispatchResult { + account.free = 1; + Ok(()) + } + )); + assert_eq!(System::providers(&ALICE), 0); + assert_eq!(Accounts::::contains_key(ALICE, DOT), false); + assert_eq!( + Tokens::accounts(&ALICE, DOT), + AccountData { + free: 0, + reserved: 0, + frozen: 0 + } + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::DustLost { + currency_id: DOT, + who: ALICE, + amount: 1, + })); + + // wipe out zero account, will not trigger DustLost event + assert_eq!(System::providers(&BOB), 1); + assert_eq!(Accounts::::contains_key(BOB, DOT), true); + assert_eq!( + Tokens::accounts(&BOB, DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 0 + } + ); + assert_ok!(Tokens::try_mutate_account(DOT, &BOB, |account, _| -> DispatchResult { + account.free = 0; + Ok(()) + })); + assert_eq!(System::providers(&BOB), 0); + assert_eq!(Accounts::::contains_key(BOB, DOT), false); + assert_eq!( + Tokens::accounts(&BOB, DOT), + AccountData { + free: 0, + reserved: 0, + frozen: 0 + } + ); + assert!(System::events().iter().all(|record| !matches!( + record.event, + RuntimeEvent::Tokens(crate::Event::DustLost { + currency_id: DOT, + who: BOB, + amount: 0 + }) + ))); + + // endow new account, will trigger Endowed event + assert_eq!(System::providers(&CHARLIE), 0); + assert_eq!(Accounts::::contains_key(CHARLIE, DOT), false); + assert_eq!( + Tokens::accounts(&CHARLIE, DOT), + AccountData { + free: 0, + reserved: 0, + frozen: 0 + } + ); + assert_ok!(Tokens::try_mutate_account( + DOT, + &CHARLIE, + |account, _| -> DispatchResult { + account.free = 50; + Ok(()) + } + )); + assert_eq!(System::providers(&CHARLIE), 1); + assert_eq!(Accounts::::contains_key(CHARLIE, DOT), true); + assert_eq!( + Tokens::accounts(&CHARLIE, DOT), + AccountData { + free: 50, + reserved: 0, + frozen: 0 + } + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Endowed { + currency_id: DOT, + who: CHARLIE, + amount: 50, + })); + + // if the account is in DustRemovalWhitelist, will not wipe out account data if + // free balance is below ED + assert_ok!(Tokens::try_mutate_account(DOT, &DAVE, |account, _| -> DispatchResult { + account.free = 1; + Ok(()) + })); + assert_eq!(System::providers(&DAVE), 1); + assert_eq!(Accounts::::contains_key(DAVE, DOT), true); + assert_eq!( + Tokens::accounts(&DAVE, DOT), + AccountData { free: 1, + reserved: 0, + frozen: 0 + } + ); + assert!(System::events().iter().all(|record| !matches!( + record.event, + RuntimeEvent::Tokens(crate::Event::DustLost { + currency_id: DOT, + who: DAVE, + amount: _ + }) + ))); + + // mutate account reserved but free is zero, will not trigger dust removal + assert_eq!(System::providers(&EVE), 1); + assert_eq!(Accounts::::contains_key(EVE, DOT), true); + assert_eq!( + Tokens::accounts(&EVE, DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 0 + } + ); + assert_ok!(Tokens::try_mutate_account(DOT, &EVE, |account, _| -> DispatchResult { + account.free = 0; + account.reserved = 1; + Ok(()) + })); + assert_eq!(System::providers(&EVE), 1); + assert_eq!(Accounts::::contains_key(EVE, DOT), true); + assert_eq!( + Tokens::accounts(&EVE, DOT), + AccountData { + free: 0, reserved: 1, frozen: 0 } - ) - .into_result(true), - Err(TokenError::FundsUnavailable.into()) - ); + ); + assert!(System::events().iter().all(|record| !matches!( + record.event, + RuntimeEvent::Tokens(crate::Event::DustLost { + currency_id: DOT, + who: EVE, + amount: _ + }) + ))); + }); +} - // less to frozen balance - assert_eq!( - Tokens::withdraw_consequence( - &ALICE, +#[test] +fn try_mutate_account_handling_dust_work() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) + .build() + .execute_with(|| { + // try_mutate_account will not handle dust + let (_, maybe_dust) = Tokens::try_mutate_account(DOT, &ALICE, |account, _| -> DispatchResult { + account.free = 1; + Ok(()) + }) + .unwrap(); + assert_eq!(System::providers(&ALICE), 0); + assert_eq!(Accounts::::contains_key(ALICE, DOT), false); + assert_eq!( + Tokens::accounts(&ALICE, DOT), + AccountData { + free: 0, + reserved: 0, + frozen: 0 + } + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::DustLost { + currency_id: DOT, + who: ALICE, + amount: 1, + })); + assert_eq!(maybe_dust, Some(1)); + assert_eq!( + Tokens::accounts(DustReceiverAccount::get(), DOT), + AccountData { + free: 0, + reserved: 0, + frozen: 0 + } + ); + + // try_mutate_account_handling_dust will handle dust + assert_ok!(Tokens::try_mutate_account_handling_dust( DOT, - 2, - &AccountData { - free: 2, + &BOB, + |account, _| -> DispatchResult { + account.free = 1; + Ok(()) + } + )); + assert_eq!(System::providers(&BOB), 0); + assert_eq!(Accounts::::contains_key(BOB, DOT), false); + assert_eq!( + Tokens::accounts(&BOB, DOT), + AccountData { + free: 0, + reserved: 0, + frozen: 0 + } + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::DustLost { + currency_id: DOT, + who: BOB, + amount: 1, + })); + assert_eq!( + Tokens::accounts(DustReceiverAccount::get(), DOT), + AccountData { + free: 1, reserved: 0, - frozen: 2 + frozen: 0 } - ) - .into_result(true), - Err(TokenError::Frozen.into()) - ); - }); + ); + }); } #[test] @@ -498,100 +715,6 @@ fn ensure_can_withdraw_should_work() { }); } -#[test] -fn set_free_balance_should_work() { - ExtBuilder::default().build().execute_with(|| { - /* Scenarios: ED is not zero, account is not in dust removal whitelist */ - assert!(!Accounts::::contains_key(ALICE, DOT)); - assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 0); - assert_eq!(Tokens::total_issuance(DOT), 0); - - // when total is below ED, account will be reaped. - Tokens::set_free_balance(DOT, &ALICE, 1); - assert!(!Accounts::::contains_key(ALICE, DOT)); - assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 1); - // set_free_balance do not change total issuance. - assert_eq!(Tokens::total_issuance(DOT), 0); - - Tokens::set_free_balance(DOT, &ALICE, 2); - assert!(Accounts::::contains_key(ALICE, DOT)); - assert_eq!(Tokens::free_balance(DOT, &ALICE), 2); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 1); - - /* Scenarios: ED is not zero, account is in dust removal whitelist */ - assert!(!Accounts::::contains_key(DAVE, DOT)); - assert_eq!(Tokens::free_balance(DOT, &DAVE), 0); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 1); - - // set zero will not create account - Tokens::set_free_balance(DOT, &DAVE, 0); - assert!(!Accounts::::contains_key(DAVE, DOT)); - - // when total is below ED, account will not be reaped. - Tokens::set_free_balance(DOT, &DAVE, 1); - assert!(Accounts::::contains_key(DAVE, DOT)); - assert_eq!(Tokens::free_balance(DOT, &DAVE), 1); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 1); - - /* Scenarios: ED is zero */ - assert!(!Accounts::::contains_key(ALICE, ETH)); - assert_eq!(Tokens::free_balance(ETH, &ALICE), 0); - assert_eq!(Tokens::free_balance(ETH, &DustReceiver::get()), 0); - - // set zero will create account - Tokens::set_free_balance(ETH, &ALICE, 0); - assert!(Accounts::::contains_key(ALICE, ETH)); - assert_eq!(Tokens::free_balance(ETH, &ALICE), 0); - assert_eq!(Tokens::free_balance(ETH, &DustReceiver::get()), 0); - }); -} - -#[test] -fn set_reserved_balance_should_work() { - ExtBuilder::default().build().execute_with(|| { - /* Scenarios: ED is not zero, account is not in dust removal whitelist */ - assert!(!Accounts::::contains_key(ALICE, DOT)); - assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::total_issuance(DOT), 0); - - // when total is below ED, account should be reaped. - Tokens::set_reserved_balance(DOT, &ALICE, 1); - // but reap it failed because failed to transfer/withdraw dust removal!!! - assert!(Accounts::::contains_key(ALICE, DOT)); - assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 1); - // set_reserved_balance do not change total issuance. - assert_eq!(Tokens::total_issuance(DOT), 0); - - Tokens::set_reserved_balance(DOT, &ALICE, 2); - assert!(Accounts::::contains_key(ALICE, DOT)); - assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 2); - - /* Scenarios: ED is not zero, account is in dust removal whitelist */ - assert!(!Accounts::::contains_key(DAVE, DOT)); - assert_eq!(Tokens::free_balance(DOT, &DAVE), 0); - - // set zero will not create account - Tokens::set_reserved_balance(DOT, &DAVE, 0); - assert!(!Accounts::::contains_key(DAVE, DOT)); - - // when total is below ED, account shouldn't be reaped. - Tokens::set_reserved_balance(DOT, &DAVE, 1); - assert!(Accounts::::contains_key(DAVE, DOT)); - assert_eq!(Tokens::reserved_balance(DOT, &DAVE), 1); - - /* Scenarios: ED is zero */ - assert!(!Accounts::::contains_key(ALICE, ETH)); - assert_eq!(Tokens::reserved_balance(ETH, &ALICE), 0); - - // set zero will create account - Tokens::set_reserved_balance(ETH, &ALICE, 0); - assert!(Accounts::::contains_key(ALICE, ETH)); - assert_eq!(Tokens::reserved_balance(ETH, &ALICE), 0); - }); -} - #[test] fn do_transfer_should_work() { ExtBuilder::default() @@ -640,7 +763,7 @@ fn do_transfer_dust_removal_when_allow_death() { .execute_with(|| { assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); assert_eq!(Tokens::free_balance(DOT, &BOB), 100); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 0); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 0); assert_ok!(Tokens::do_transfer( DOT, @@ -651,7 +774,7 @@ fn do_transfer_dust_removal_when_allow_death() { )); assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); assert_eq!(Tokens::free_balance(DOT, &BOB), 199); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 1); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 1); }); } @@ -667,14 +790,7 @@ fn do_transfer_report_keep_alive_error_when_ed_is_not_zero() { Error::::KeepAlive ); - // even if dave is in dust removal whitelist, but account drain will still cause - // account be be reaped. - assert_noop!( - Tokens::do_transfer(DOT, &DAVE, &BOB, 100, ExistenceRequirement::KeepAlive), - Error::::KeepAlive - ); - - // as long as do not transfer all balance, even if the total is below ED, the + // if account is in DustRemovalWhitelist, even if the total is below ED, the // account will not be reaped. assert_eq!(Tokens::free_balance(DOT, &DAVE), 100); assert_eq!(Tokens::free_balance(DOT, &BOB), 0); @@ -837,7 +953,7 @@ fn do_withdraw_dust_removal_when_allow_death() { assert_eq!(Tokens::total_issuance(DOT), 100); assert!(Accounts::::contains_key(ALICE, DOT)); assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 0); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 0); assert_ok!(Tokens::do_withdraw( DOT, @@ -849,7 +965,7 @@ fn do_withdraw_dust_removal_when_allow_death() { assert_eq!(Tokens::total_issuance(DOT), 1); assert!(!Accounts::::contains_key(ALICE, DOT)); assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 1); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 1); }); } @@ -879,13 +995,6 @@ fn do_withdraw_report_keep_alive_error_when_ed_is_not_zero() { assert!(Accounts::::contains_key(DAVE, DOT)); assert_eq!(Tokens::free_balance(DOT, &DAVE), 1); assert_eq!(Tokens::total_issuance(DOT), 101); - - // even if dave is in dust removal whitelist, but if withdraw all total of it - // will still cause account reaped. - assert_noop!( - Tokens::do_withdraw(DOT, &DAVE, 1, ExistenceRequirement::KeepAlive, true), - Error::::KeepAlive - ); }); } @@ -990,6 +1099,459 @@ fn do_deposit_report_existential_deposit_error() { }); } +#[test] +fn update_locks_works() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100)]) + .build() + .execute_with(|| { + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 0, + } + ); + assert_eq!(Tokens::locks(&ALICE, &DOT), vec![]); + assert_eq!(System::consumers(&ALICE), 0); + + assert_ok!(Tokens::update_locks( + DOT, + &ALICE, + &vec![BalanceLock { id: ID_1, amount: 30 }] + )); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Locked { + currency_id: DOT, + who: ALICE, + amount: 30, + })); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 30, + } + ); + assert_eq!(Tokens::locks(&ALICE, &DOT), vec![BalanceLock { id: ID_1, amount: 30 }]); + assert_eq!(System::consumers(&ALICE), 1); + + assert_ok!(Tokens::update_locks( + DOT, + &ALICE, + &vec![ + BalanceLock { id: ID_1, amount: 30 }, + BalanceLock { id: ID_2, amount: 35 } + ] + )); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Locked { + currency_id: DOT, + who: ALICE, + amount: 5, + })); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 35, + } + ); + assert_eq!( + Tokens::locks(&ALICE, &DOT), + vec![ + BalanceLock { id: ID_1, amount: 30 }, + BalanceLock { id: ID_2, amount: 35 } + ] + ); + assert_eq!(System::consumers(&ALICE), 1); + + assert_noop!( + Tokens::update_locks( + DOT, + &ALICE, + &vec![ + BalanceLock { id: ID_1, amount: 30 }, + BalanceLock { id: ID_2, amount: 35 }, + BalanceLock { id: ID_3, amount: 40 }, + ] + ), + Error::::MaxLocksExceeded + ); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 35, + } + ); + assert_eq!( + Tokens::locks(&ALICE, &DOT), + vec![ + BalanceLock { id: ID_1, amount: 30 }, + BalanceLock { id: ID_2, amount: 35 } + ] + ); + assert_eq!(System::consumers(&ALICE), 1); + + assert_ok!(Tokens::update_locks(DOT, &ALICE, &vec![])); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Unlocked { + currency_id: DOT, + who: ALICE, + amount: 35, + })); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 0, + } + ); + assert_eq!(Tokens::locks(&ALICE, &DOT), vec![]); + assert_eq!(System::consumers(&ALICE), 0); + }); +} + +#[test] +fn do_transfer_reserved_works() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100)]) + .build() + .execute_with(|| { + assert_ok!(Tokens::reserve(DOT, &ALICE, 80)); + assert_ok!(Tokens::update_locks( + DOT, + &ALICE, + &vec![BalanceLock { id: ID_1, amount: 30 }] + )); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 20, + reserved: 80, + frozen: 30, + } + ); + + // case 1: + // slashed == beneficiary + // Precision::Exact + // Fortitude::Polite, The freeze lock applies to the total balance, if discount + // free balance the remaining is not zero, will locks reserved balance also + // BalanceStatus::Free + // amount is 80, but avaliable is 70, will fail + assert_noop!( + Tokens::do_transfer_reserved( + DOT, + &ALICE, + &ALICE, + 80, + Precision::Exact, + Fortitude::Polite, + BalanceStatus::Free + ), + TokenError::FundsUnavailable + ); + + // case 2: + // slashed == beneficiary + // Precision::BestEffort + // Fortitude::Polite, The freeze lock applies to the total balance, if discount + // free balance the remaining is not zero, will locks reserved balance also + // BalanceStatus::Free + // amount is 80, avaliable is 70, actual is 70 + // ALICE will unreserve 70 + assert_eq!( + Tokens::do_transfer_reserved( + DOT, + &ALICE, + &ALICE, + 80, + Precision::BestEffort, + Fortitude::Polite, + BalanceStatus::Free + ), + Ok(70) + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Unreserved { + currency_id: DOT, + who: ALICE, + amount: 70, + })); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 90, + reserved: 10, + frozen: 30, + } + ); + + // revert to origin state + assert_ok!(Tokens::update_locks(DOT, &ALICE, &vec![])); + assert_ok!(Tokens::reserve(DOT, &ALICE, 70)); + assert_ok!(Tokens::update_locks( + DOT, + &ALICE, + &vec![BalanceLock { id: ID_1, amount: 30 }] + )); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 20, + reserved: 80, + frozen: 30, + } + ); + + // case 3: + // slashed == beneficiary + // Precision::Exact + // Fortitude::Force + // BalanceStatus::Free + // amount is 80, but avaliable is 80 + // ALICE will unreserve 80 + assert_eq!( + Tokens::do_transfer_reserved( + DOT, + &ALICE, + &ALICE, + 80, + Precision::Exact, + Fortitude::Force, + BalanceStatus::Free + ), + Ok(80) + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Unreserved { + currency_id: DOT, + who: ALICE, + amount: 80, + })); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 30, + } + ); + + // revert to origin state + assert_ok!(Tokens::update_locks(DOT, &ALICE, &vec![])); + assert_ok!(Tokens::reserve(DOT, &ALICE, 80)); + assert_ok!(Tokens::update_locks( + DOT, + &ALICE, + &vec![BalanceLock { id: ID_1, amount: 30 }] + )); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 20, + reserved: 80, + frozen: 30, + } + ); + + // case 4: + // slashed == beneficiary + // Precision::BestEffort + // Fortitude::Force + // BalanceStatus::Free + // amount is 100, but avaliable is 80 + // ALICE will unreserve 80 + assert_eq!( + Tokens::do_transfer_reserved( + DOT, + &ALICE, + &ALICE, + 100, + Precision::BestEffort, + Fortitude::Force, + BalanceStatus::Free + ), + Ok(80) + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Unreserved { + currency_id: DOT, + who: ALICE, + amount: 80, + })); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 100, + reserved: 0, + frozen: 30, + } + ); + + // revert to origin state + assert_ok!(Tokens::update_locks(DOT, &ALICE, &vec![])); + assert_ok!(Tokens::reserve(DOT, &ALICE, 80)); + assert_ok!(Tokens::update_locks( + DOT, + &ALICE, + &vec![BalanceLock { id: ID_1, amount: 30 }] + )); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 20, + reserved: 80, + frozen: 30, + } + ); + + // case 5: + // slashed == beneficiary + // Precision::BestEffort + // Fortitude::Force + // BalanceStatus::Reserved + // amount is 100, but avaliable is 80 + // nothing happen for ALICE + assert_eq!( + Tokens::do_transfer_reserved( + DOT, + &ALICE, + &ALICE, + 100, + Precision::BestEffort, + Fortitude::Force, + BalanceStatus::Reserved + ), + Ok(80) + ); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 20, + reserved: 80, + frozen: 30, + } + ); + + // case 6: + // slashed == beneficiary + // Precision::Exact + // Fortitude::Force + // BalanceStatus::Reserved + // amount is 100, but avaliable is 80 + // throw error + assert_noop!( + Tokens::do_transfer_reserved( + DOT, + &ALICE, + &ALICE, + 100, + Precision::Exact, + Fortitude::Force, + BalanceStatus::Reserved + ), + TokenError::FundsUnavailable + ); + + assert_eq!( + Tokens::accounts(&BOB, &DOT), + AccountData { + free: 0, + reserved: 0, + frozen: 0, + } + ); + + // case 7: + // slashed != beneficiary + // Precision::Exact + // Fortitude::Force + // BalanceStatus::Reserved + // amount is 20, avaliable is 80 + // ALICE's reserved balance will transfer 20 to BOB's reserved balance + assert_eq!( + Tokens::do_transfer_reserved( + DOT, + &ALICE, + &BOB, + 20, + Precision::Exact, + Fortitude::Force, + BalanceStatus::Reserved + ), + Ok(20) + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::ReserveRepatriated { + currency_id: DOT, + from: ALICE, + to: BOB, + amount: 20, + status: BalanceStatus::Reserved, + })); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 20, + reserved: 60, + frozen: 30, + } + ); + assert_eq!( + Tokens::accounts(&BOB, &DOT), + AccountData { + free: 0, + reserved: 20, + frozen: 0, + } + ); + + // case 8: + // slashed != beneficiary + // Precision::Exact + // Fortitude::Force + // BalanceStatus::Free + // amount is 20, avaliable is 60 + // ALICE's reserved balance will transfer 20 to BOB's free balance + assert_eq!( + Tokens::do_transfer_reserved( + DOT, + &ALICE, + &BOB, + 20, + Precision::Exact, + Fortitude::Force, + BalanceStatus::Free + ), + Ok(20) + ); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::ReserveRepatriated { + currency_id: DOT, + from: ALICE, + to: BOB, + amount: 20, + status: BalanceStatus::Free, + })); + assert_eq!( + Tokens::accounts(&ALICE, &DOT), + AccountData { + free: 20, + reserved: 40, + frozen: 30, + } + ); + assert_eq!( + Tokens::accounts(&BOB, &DOT), + AccountData { + free: 20, + reserved: 20, + frozen: 0, + } + ); + }); +} + // ************************************************* // tests for endowed account and remove account // ************************************************* @@ -999,8 +1561,8 @@ fn endowed_account_work() { ExtBuilder::default().build().execute_with(|| { assert_eq!(System::providers(&ALICE), 0); assert!(!Accounts::::contains_key(ALICE, DOT)); - Tokens::set_free_balance(DOT, &ALICE, 100); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Endowed { + assert_ok!(Tokens::set_balance(RawOrigin::Root.into(), ALICE, DOT, 100, 0)); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Endowed { currency_id: DOT, who: ALICE, amount: 100, @@ -1018,7 +1580,7 @@ fn remove_account_work() { .execute_with(|| { assert_eq!(System::providers(&ALICE), 1); assert!(Accounts::::contains_key(ALICE, DOT)); - Tokens::set_free_balance(DOT, &ALICE, 0); + assert_ok!(Tokens::set_balance(RawOrigin::Root.into(), ALICE, DOT, 0, 0)); assert_eq!(System::providers(&ALICE), 0); assert!(!Accounts::::contains_key(ALICE, DOT)); }); @@ -1082,28 +1644,32 @@ fn dust_removal_work() { assert_eq!(System::providers(&ALICE), 1); assert!(Accounts::::contains_key(ALICE, DOT)); assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 0); - Tokens::set_free_balance(DOT, &ALICE, 1); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::DustLost { + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 0); + assert_eq!(Tokens::total_issuance(DOT), 100); + + // set_balance cannot set free_balance below ED, will set 0 + assert_ok!(Tokens::set_balance(RawOrigin::Root.into(), ALICE, DOT, 1, 0)); + System::assert_last_event(RuntimeEvent::Tokens(crate::Event::BalanceSet { currency_id: DOT, who: ALICE, - amount: 1, + free: 0, + reserved: 0, })); assert_eq!(System::providers(&ALICE), 0); assert!(!Accounts::::contains_key(ALICE, DOT)); assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::free_balance(DOT, &DustReceiver::get()), 1); + assert_eq!(Tokens::free_balance(DOT, &DustReceiverAccount::get()), 0); + assert_eq!(Tokens::total_issuance(DOT), 0); - // dave is in dust removal whitelist, will not remove its dust even if its total - // below ED - assert!(!Accounts::::contains_key(DAVE, DOT)); + // dave is in dust removal whitelist, will not wipeout assert_eq!(System::providers(&DAVE), 0); + assert!(!Accounts::::contains_key(DAVE, DOT)); assert_eq!(Tokens::free_balance(DOT, &DAVE), 0); - Tokens::set_free_balance(DOT, &DAVE, 1); + assert_ok!(Tokens::set_balance(RawOrigin::Root.into(), DAVE, DOT, 1, 0)); assert!(Accounts::::contains_key(DAVE, DOT)); assert_eq!(System::providers(&DAVE), 1); assert_eq!(Tokens::free_balance(DOT, &DAVE), 1); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Endowed { + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::Endowed { currency_id: DOT, who: DAVE, amount: 1, @@ -1114,23 +1680,31 @@ fn dust_removal_work() { #[test] fn account_survive_due_to_dust_transfer_failure() { ExtBuilder::default().build().execute_with(|| { - let dust_account = DustReceiver::get(); - Tokens::set_free_balance(DOT, &dust_account, 0); + let dust_account = DustReceiverAccount::get(); + assert_ok!(Tokens::set_balance( + RawOrigin::Root.into(), + dust_account.clone(), + DOT, + 0, + 0 + )); assert_eq!(Tokens::free_balance(DOT, &dust_account), 0); assert_eq!(Tokens::total_balance(DOT, &ALICE), 0); assert_eq!(System::providers(&ALICE), 0); assert!(!Accounts::::contains_key(ALICE, DOT)); - Tokens::set_reserved_balance(DOT, &ALICE, 1); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::DustLost { + // set_balance will set zero if the amount will cause wipeout + assert_ok!(Tokens::set_balance(RawOrigin::Root.into(), ALICE, DOT, 1, 0)); + System::assert_has_event(RuntimeEvent::Tokens(crate::Event::BalanceSet { currency_id: DOT, who: ALICE, - amount: 1, + free: 0, + reserved: 0, })); assert_eq!(Tokens::free_balance(DOT, &dust_account), 0); - assert_eq!(Tokens::total_balance(DOT, &ALICE), 1); - assert_eq!(System::providers(&ALICE), 1); - assert!(Accounts::::contains_key(ALICE, DOT)); + assert_eq!(Tokens::total_balance(DOT, &ALICE), 0); + assert_eq!(System::providers(&ALICE), 0); + assert!(!Accounts::::contains_key(ALICE, DOT)); }); } diff --git a/tokens/src/tests_currency_adapter.rs b/tokens/src/tests_currency_adapter.rs index c0ced63a3..94b61c3a1 100644 --- a/tokens/src/tests_currency_adapter.rs +++ b/tokens/src/tests_currency_adapter.rs @@ -3,7 +3,10 @@ #![cfg(test)] use super::*; -use frame_support::{assert_noop, assert_ok}; +use frame_support::{ + assert_noop, assert_ok, + traits::{Currency, LockableCurrency, NamedReservableCurrency, ReservableCurrency, WithdrawReasons}, +}; use mock::*; #[test] @@ -16,6 +19,7 @@ fn currency_adapter_ensure_currency_adapter_should_work() { assert_eq!(Tokens::total_balance(DOT, &TREASURY_ACCOUNT), 100); assert_eq!(Tokens::reserved_balance(DOT, &TREASURY_ACCOUNT), 0); assert_eq!(Tokens::free_balance(DOT, &TREASURY_ACCOUNT), 100); + assert_eq!( ::Currency::total_balance(&TREASURY_ACCOUNT), 100 @@ -192,35 +196,25 @@ fn currency_adapter_deducting_balance_should_work() { fn currency_adapter_refunding_balance_should_work() { ExtBuilder::default().build().execute_with(|| { let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 42); - Tokens::set_reserved_balance(DOT, &TREASURY_ACCOUNT, 69); + Tokens::mutate_account_handling_dust(DOT, &TREASURY_ACCOUNT, |account| { + account.reserved = 69; + }); TreasuryCurrencyAdapter::unreserve(&TREASURY_ACCOUNT, 69); assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 111); assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 0); }); } -#[test] -fn currency_adapter_slashing_balance_should_work() { - ExtBuilder::default().build().execute_with(|| { - let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 111); - assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 69)); - assert!(TreasuryCurrencyAdapter::slash(&TREASURY_ACCOUNT, 69).1.is_zero()); - assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 0); - assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 42); - assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 42); - }); -} - #[test] fn currency_adapter_slashing_incomplete_balance_should_work() { ExtBuilder::default().build().execute_with(|| { let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 42); assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 42); assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 21)); - assert_eq!(TreasuryCurrencyAdapter::slash(&TREASURY_ACCOUNT, 69).1, 27); + assert_eq!(TreasuryCurrencyAdapter::slash(&TREASURY_ACCOUNT, 69).1, 48); assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 0); - assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 0); - assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 0); + assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 21); + assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 21); }); } @@ -481,7 +475,7 @@ fn currency_adapter_repatriating_reserved_balance_should_work() { let _ = TreasuryCurrencyAdapter::deposit_creating(&ALICE, 2); assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 110)); assert_ok!( - TreasuryCurrencyAdapter::repatriate_reserved(&TREASURY_ACCOUNT, &ALICE, 41, Status::Free), + TreasuryCurrencyAdapter::repatriate_reserved(&TREASURY_ACCOUNT, &ALICE, 41, BalanceStatus::Free), 0 ); assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 69); @@ -498,7 +492,7 @@ fn currency_adapter_transferring_reserved_balance_should_work() { let _ = TreasuryCurrencyAdapter::deposit_creating(&ALICE, 2); assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 110)); assert_ok!( - TreasuryCurrencyAdapter::repatriate_reserved(&TREASURY_ACCOUNT, &ALICE, 41, Status::Reserved), + TreasuryCurrencyAdapter::repatriate_reserved(&TREASURY_ACCOUNT, &ALICE, 41, BalanceStatus::Reserved), 0 ); assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 69); @@ -517,7 +511,7 @@ fn currency_adapter_transferring_reserved_balance_to_nonexistent_should_fail() { &TREASURY_ACCOUNT, &ALICE, 42, - Status::Free + BalanceStatus::Free )); }); } @@ -529,7 +523,7 @@ fn currency_adapter_transferring_incomplete_reserved_balance_should_work() { let _ = TreasuryCurrencyAdapter::deposit_creating(&ALICE, 2); assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 41)); assert_ok!( - TreasuryCurrencyAdapter::repatriate_reserved(&TREASURY_ACCOUNT, &ALICE, 69, Status::Free), + TreasuryCurrencyAdapter::repatriate_reserved(&TREASURY_ACCOUNT, &ALICE, 69, BalanceStatus::Free), 28 ); assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 0); @@ -626,7 +620,13 @@ fn currency_adapter_repatriating_named_reserved_balance_should_work() { let _ = TreasuryCurrencyAdapter::deposit_creating(&ALICE, 2); assert_ok!(TreasuryCurrencyAdapter::reserve_named(&RID_1, &TREASURY_ACCOUNT, 110)); assert_ok!( - TreasuryCurrencyAdapter::repatriate_reserved_named(&RID_1, &TREASURY_ACCOUNT, &ALICE, 41, Status::Free), + TreasuryCurrencyAdapter::repatriate_reserved_named( + &RID_1, + &TREASURY_ACCOUNT, + &ALICE, + 41, + BalanceStatus::Free + ), 0 ); assert_eq!( diff --git a/tokens/src/tests_events.rs b/tokens/src/tests_events.rs index c75c9b0d8..d28292111 100644 --- a/tokens/src/tests_events.rs +++ b/tokens/src/tests_events.rs @@ -3,7 +3,10 @@ #![cfg(test)] use super::*; -use frame_support::assert_ok; +use frame_support::{ + assert_ok, + traits::{tokens::Restriction, Currency as PalletCurrency}, +}; use mock::*; const REASON: &() = &(); @@ -43,12 +46,12 @@ fn pallet_multicurrency_deposit_events() { })); assert_ok!(>::reserve(DOT, &ALICE, 50)); - assert_eq!(>::slash(DOT, &ALICE, 60), 0); + assert_eq!(>::slash(DOT, &ALICE, 60), 20); System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Slashed { currency_id: DOT, who: ALICE, free_amount: 40, - reserved_amount: 20, + reserved_amount: 0, })); }); } @@ -207,31 +210,6 @@ fn pallet_fungibles_transfer_deposit_events() { }); } -#[test] -fn pallet_fungibles_unbalanced_deposit_events() { - ExtBuilder::default() - .balances(vec![(ALICE, DOT, 100)]) - .build() - .execute_with(|| { - assert_ok!(>::reserve(DOT, &ALICE, 50)); - assert_ok!(>::write_balance( - DOT, &ALICE, 500 - )); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::BalanceSet { - currency_id: DOT, - who: ALICE, - free: 500, - reserved: 50, - })); - - >::set_total_issuance(DOT, 1000); - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::TotalIssuanceSet { - currency_id: DOT, - amount: 1000, - })); - }); -} - #[test] fn pallet_fungibles_mutate_hold_deposit_events() { ExtBuilder::default() @@ -310,12 +288,12 @@ fn currency_adapter_pallet_currency_deposit_events() { })); assert_ok!(>::reserve(DOT, &BOB, 50)); - std::mem::forget(>::slash(&BOB, 110)); + std::mem::forget(>::slash(&BOB, 100)); System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Slashed { currency_id: DOT, who: BOB, free_amount: 100, - reserved_amount: 10, + reserved_amount: 0, })); std::mem::forget(>::make_free_balance_be(&BOB, 200)); @@ -323,7 +301,7 @@ fn currency_adapter_pallet_currency_deposit_events() { currency_id: DOT, who: BOB, free: 200, - reserved: 40, + reserved: 50, })); }); } diff --git a/tokens/src/tests_fungibles.rs b/tokens/src/tests_fungibles.rs index d7b6abf55..e74d722df 100644 --- a/tokens/src/tests_fungibles.rs +++ b/tokens/src/tests_fungibles.rs @@ -3,7 +3,7 @@ #![cfg(test)] use super::*; -use frame_support::{assert_noop, assert_ok}; +use frame_support::{assert_noop, assert_ok, traits::tokens::Restriction}; use mock::*; use sp_runtime::{ArithmeticError, TokenError}; @@ -135,7 +135,7 @@ fn fungibles_unbalanced_trait_should_work() { Preservation::Protect, Fortitude::Polite ), - 50 + 48 ); assert_noop!( >::decrease_balance( @@ -226,7 +226,7 @@ fn fungibles_unbalanced_trait_should_work() { Preservation::Protect, Fortitude::Polite ), - 50 + 48 ); assert_eq!( >::decrease_balance( @@ -237,16 +237,16 @@ fn fungibles_unbalanced_trait_should_work() { Preservation::Protect, Fortitude::Polite ), - Ok(50), + Ok(48), ); - assert_eq!(>::balance(DOT, &ALICE), 0); - assert_eq!(>::total_balance(DOT, &ALICE), 50); + assert_eq!(>::balance(DOT, &ALICE), 2); + assert_eq!(>::total_balance(DOT, &ALICE), 52); assert_eq!( >::unreserve(DOT, &ALICE, 50), 0 ); - assert_eq!(>::balance(DOT, &ALICE), 50); - assert_eq!(>::total_balance(DOT, &ALICE), 50); + assert_eq!(>::balance(DOT, &ALICE), 52); + assert_eq!(>::total_balance(DOT, &ALICE), 52); // increase_balance assert_ok!(>::write_balance(DOT, &ALICE, 0)); @@ -408,7 +408,7 @@ fn fungibles_mutate_hold_trait_should_work() { .execute_with(|| { assert_noop!( >::hold(DOT, REASON, &ALICE, 200), - Error::::BalanceTooLow + TokenError::FundsUnavailable ); assert_eq!( >::balance_on_hold(DOT, REASON, &ALICE), @@ -416,12 +416,18 @@ fn fungibles_mutate_hold_trait_should_work() { ); assert_eq!(>::balance(DOT, &ALICE), 100); - assert_ok!(>::hold(DOT, REASON, &ALICE, 100)); + // must keep free >= ed + assert_noop!( + >::hold(DOT, REASON, &ALICE, 100), + TokenError::FundsUnavailable + ); + + assert_ok!(>::hold(DOT, REASON, &ALICE, 90)); assert_eq!( >::balance_on_hold(DOT, REASON, &ALICE), - 100 + 90 ); - assert_eq!(>::balance(DOT, &ALICE), 0); + assert_eq!(>::balance(DOT, &ALICE), 10); assert_eq!( >::release(DOT, REASON, &ALICE, 40, Precision::Exact), @@ -429,20 +435,20 @@ fn fungibles_mutate_hold_trait_should_work() { ); assert_eq!( >::balance_on_hold(DOT, REASON, &ALICE), - 60 + 50 ); - assert_eq!(>::balance(DOT, &ALICE), 40); + assert_eq!(>::balance(DOT, &ALICE), 50); // exceed hold amount when not in best_effort assert_noop!( - >::release(DOT, REASON, &ALICE, 61, Precision::Exact), - Error::::BalanceTooLow + >::release(DOT, REASON, &ALICE, 51, Precision::Exact), + TokenError::FundsUnavailable ); // exceed hold amount when in best_effort assert_eq!( - >::release(DOT, REASON, &ALICE, 61, Precision::BestEffort), - Ok(60) + >::release(DOT, REASON, &ALICE, 51, Precision::BestEffort), + Ok(50) ); assert_eq!( >::balance_on_hold(DOT, REASON, &ALICE), @@ -522,7 +528,7 @@ fn fungibles_mutate_hold_trait_should_work() { Restriction::OnHold, Fortitude::Polite ), - Error::::BalanceTooLow + TokenError::Frozen ); // exceed hold amount when in best_effort diff --git a/tokens/src/tests_fungibles_conformance.rs b/tokens/src/tests_fungibles_conformance.rs new file mode 100644 index 000000000..090455270 --- /dev/null +++ b/tokens/src/tests_fungibles_conformance.rs @@ -0,0 +1,75 @@ +#![cfg(test)] + +use super::*; +use crate::fungibles_conformance_tests; +use mock::*; +use paste::paste; + +macro_rules! run_tests { + ($path:path, $currency_id:expr, $($name:ident),*) => { + $( + paste! { + #[test] + fn [< $name _ $currency_id _dust_trap_on >]() { + let trap_account = DustReceiverAccount::get(); + let builder = ExtBuilder::default(); + builder.build().execute_with(|| { + >::set_balance($currency_id, &trap_account, Tokens::minimum_balance($currency_id)); + $path::$name::< + Tokens, + ::AccountId, + >($currency_id, Some(trap_account)); + }); + } + + #[test] + fn [< $name _ $currency_id _dust_trap_off >]() { + let trap_account = DustReceiverAccount::get(); + let builder = ExtBuilder::default(); + builder.build().execute_with(|| { + GetDustReceiverAccount::set(None); + $path::$name::< + Tokens, + ::AccountId, + >($currency_id, None); + }); + } + } + )* + }; + ($path:path, $currency_id:expr) => { + run_tests!( + $path, + $currency_id, + mint_into_success, + mint_into_overflow, + mint_into_below_minimum, + burn_from_exact_success, + burn_from_best_effort_success, + burn_from_exact_insufficient_funds, + restore_success, + restore_overflow, + restore_below_minimum, + shelve_success, + shelve_insufficient_funds, + transfer_success, + transfer_expendable_all, + transfer_expendable_dust, + transfer_protect_preserve, + set_balance_mint_success, + set_balance_burn_success, + can_deposit_success, + can_deposit_below_minimum, + can_deposit_overflow, + can_withdraw_success, + can_withdraw_reduced_to_zero, + can_withdraw_balance_low, + reducible_balance_expendable, + reducible_balance_protect_preserve + ); + }; +} + +run_tests!(fungibles_conformance_tests::inspect_mutate, DOT); +run_tests!(fungibles_conformance_tests::inspect_mutate, BTC); +run_tests!(fungibles_conformance_tests::inspect_mutate, ETH); diff --git a/tokens/src/tests_multicurrency.rs b/tokens/src/tests_multicurrency.rs index 0db833786..d6be2fdd5 100644 --- a/tokens/src/tests_multicurrency.rs +++ b/tokens/src/tests_multicurrency.rs @@ -108,7 +108,7 @@ fn multi_lockable_currency_set_lock_work() { .execute_with(|| { assert_ok!(Tokens::set_lock(ID_1, DOT, &ALICE, 10)); assert_eq!(Tokens::accounts(&ALICE, DOT).frozen, 10); - assert_eq!(Tokens::accounts(&ALICE, DOT).frozen(), 10); + assert_eq!(Tokens::accounts(&ALICE, DOT).frozen, 10); assert_eq!(Tokens::locks(ALICE, DOT).len(), 1); assert_ok!(Tokens::set_lock(ID_1, DOT, &ALICE, 50)); assert_eq!(Tokens::accounts(&ALICE, DOT).frozen, 50); @@ -288,12 +288,6 @@ fn multi_reservable_currency_repatriate_reserved_work() { Tokens::repatriate_reserved(DOT, &ALICE, &ALICE, 50, BalanceStatus::Free), Ok(50) ); - // Repatriating from and to the same account, fund is `unreserved`. - System::assert_last_event(RuntimeEvent::Tokens(crate::Event::Unreserved { - currency_id: DOT, - who: ALICE, - amount: 0, - })); assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 0); @@ -350,7 +344,7 @@ fn multi_reservable_currency_repatriate_reserved_work() { } #[test] -fn slash_draw_reserved_correct() { +fn slash_cannot_draw_reserved() { ExtBuilder::default() .balances(vec![(ALICE, DOT, 100)]) .build() @@ -360,15 +354,15 @@ fn slash_draw_reserved_correct() { assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 50); assert_eq!(Tokens::total_issuance(DOT), 100); - assert_eq!(Tokens::slash(DOT, &ALICE, 80), 0); + assert_eq!(Tokens::slash(DOT, &ALICE, 80), 30); assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 20); - assert_eq!(Tokens::total_issuance(DOT), 20); + assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 50); + assert_eq!(Tokens::total_issuance(DOT), 50); - assert_eq!(Tokens::slash(DOT, &ALICE, 50), 30); + assert_eq!(Tokens::slash(DOT, &ALICE, 50), 50); assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 0); - assert_eq!(Tokens::total_issuance(DOT), 0); + assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 50); + assert_eq!(Tokens::total_issuance(DOT), 50); }); } diff --git a/traits/src/currency.rs b/traits/src/currency.rs index 0d50e7477..8893a400e 100644 --- a/traits/src/currency.rs +++ b/traits/src/currency.rs @@ -638,11 +638,11 @@ pub trait NamedBasicReservableCurrency: BasicReser /// Handler for account which has dust, need to burn or recycle it pub trait OnDust { - fn on_dust(who: &AccountId, currency_id: CurrencyId, amount: Balance); + fn on_dust(currency_id: CurrencyId, who: &AccountId, amount: Balance); } impl OnDust for () { - fn on_dust(_: &AccountId, _: CurrencyId, _: Balance) {} + fn on_dust(_: CurrencyId, _: &AccountId, _: Balance) {} } pub trait TransferAll { @@ -692,7 +692,7 @@ impl OnTransfer } pub trait MutationHooks { - /// Handler to burn or transfer account's dust. + /// Handler to when account's dust lost. type OnDust: OnDust; /// Hook to run before slashing an account. diff --git a/xtokens/src/mock/para.rs b/xtokens/src/mock/para.rs index b3890877f..19d85ebc9 100644 --- a/xtokens/src/mock/para.rs +++ b/xtokens/src/mock/para.rs @@ -90,6 +90,7 @@ impl orml_tokens::Config for Runtime { type MaxReserves = ConstU32<50>; type ReserveIdentifier = [u8; 8]; type DustRemovalWhitelist = Everything; + type DustRemoval = (); } parameter_types! { diff --git a/xtokens/src/mock/para_relative_view.rs b/xtokens/src/mock/para_relative_view.rs index 38bb3b95c..4b41d2953 100644 --- a/xtokens/src/mock/para_relative_view.rs +++ b/xtokens/src/mock/para_relative_view.rs @@ -93,6 +93,7 @@ impl orml_tokens::Config for Runtime { type MaxReserves = ConstU32<50>; type ReserveIdentifier = [u8; 8]; type DustRemovalWhitelist = Everything; + type DustRemoval = (); } parameter_types! { diff --git a/xtokens/src/mock/para_teleport.rs b/xtokens/src/mock/para_teleport.rs index fe6c37be6..fa3015e51 100644 --- a/xtokens/src/mock/para_teleport.rs +++ b/xtokens/src/mock/para_teleport.rs @@ -91,6 +91,7 @@ impl orml_tokens::Config for Runtime { type MaxReserves = ConstU32<50>; type ReserveIdentifier = [u8; 8]; type DustRemovalWhitelist = Everything; + type DustRemoval = (); } parameter_types! {