I started this investigation/issue based on @liamaharon question [here](https://github.com/paritytech/polkadot-sdk/pull/1801#discussion_r1410452499). ## Problem The `pallet_balances` integrity test should correctly detect that the runtime has correct distinct `HoldReasons` variant count. I assume the same situation exists for RuntimeFreezeReason. It is not a critical problem, if we set `MaxHolds` with a sufficiently large value, everything should be ok. However, in this case, the integrity_test check becomes less useful. **Situation for "any" runtime:** - `HoldReason` enums from different pallets: ```rust /// from pallet_nis #[pallet::composite_enum] pub enum HoldReason { NftReceipt, } /// from pallet_preimage #[pallet::composite_enum] pub enum HoldReason { Preimage, } // from pallet_state-trie-migration #[pallet::composite_enum] pub enum HoldReason { SlashForContinueMigrate, SlashForMigrateCustomTop, SlashForMigrateCustomChild, } ``` - generated `RuntimeHoldReason` enum looks like: ```rust pub enum RuntimeHoldReason { #[codec(index = 32u8)] Preimage(pallet_preimage::HoldReason), #[codec(index = 38u8)] Nis(pallet_nis::HoldReason), #[codec(index = 42u8)] StateTrieMigration(pallet_state_trie_migration::HoldReason), } ``` - composite enum `RuntimeHoldReason` variant count is detected as `3` - we set `type MaxHolds = ConstU32<3>` - `pallet_balances::integrity_test` is ok with `3`(at least 3) However, the real problem can occur in a live runtime where some functionality might stop working. This is due to a total of 5 distinct hold reasons (for pallets with multi-instance support, it is even more), and not all of them can be used because of an incorrect `MaxHolds`, which is deemed acceptable according to the `integrity_test`: ``` // pseudo-code - if we try to call all of these: T::Currency::hold(&pallet_nis::HoldReason::NftReceipt.into(), &nft_owner, deposit)?; T::Currency::hold(&pallet_preimage::HoldReason::Preimage.into(), &nft_owner, deposit)?; T::Currency::hold(&pallet_state_trie_migration::HoldReason::SlashForContinueMigrate.into(), &nft_owner, deposit)?; // With `type MaxHolds = ConstU32<3>` these two will fail T::Currency::hold(&pallet_state_trie_migration::HoldReason::SlashForMigrateCustomTop.into(), &nft_owner, deposit)?; T::Currency::hold(&pallet_state_trie_migration::HoldReason::SlashForMigrateCustomChild.into(), &nft_owner, deposit)?; ``` ## Solutions A macro `#[pallet::*]` expansion is extended of `VariantCount` implementation for the `#[pallet::composite_enum]` enum type. This expansion generates the `VariantCount` implementation for pallets' `HoldReason`, `FreezeReason`, `LockId`, and `SlashReason`. Enum variants must be plain enum values without fields to ensure a deterministic count. The composite runtime enum, `RuntimeHoldReason` and `RuntimeFreezeReason`, now sets `VariantCount::VARIANT_COUNT` as the sum of pallets' enum `VariantCount::VARIANT_COUNT`: ```rust #[frame_support::pallet(dev_mode)] mod module_single_instance { #[pallet::composite_enum] pub enum HoldReason { ModuleSingleInstanceReason1, ModuleSingleInstanceReason2, } ... } #[frame_support::pallet(dev_mode)] mod module_multi_instance { #[pallet::composite_enum] pub enum HoldReason<I: 'static = ()> { ModuleMultiInstanceReason1, ModuleMultiInstanceReason2, ModuleMultiInstanceReason3, } ... } impl self::sp_api_hidden_includes_construct_runtime::hidden_include::traits::VariantCount for RuntimeHoldReason { const VARIANT_COUNT: u32 = 0 + module_single_instance::HoldReason::VARIANT_COUNT + module_multi_instance::HoldReason::<module_multi_instance::Instance1>::VARIANT_COUNT + module_multi_instance::HoldReason::<module_multi_instance::Instance2>::VARIANT_COUNT + module_multi_instance::HoldReason::<module_multi_instance::Instance3>::VARIANT_COUNT; } ``` In addition, `MaxHolds` is removed (as suggested [here](https://github.com/paritytech/polkadot-sdk/pull/2657#discussion_r1443324573)) from `pallet_balances`, and its `Holds` are now bounded to `RuntimeHoldReason::VARIANT_COUNT`. Therefore, there is no need to let the runtime specify `MaxHolds`. ## For reviewers Relevant changes can be found here: - `substrate/frame/support/procedural/src/lib.rs` - `substrate/frame/support/procedural/src/pallet/parse/composite.rs` - `substrate/frame/support/procedural/src/pallet/expand/composite.rs` - `substrate/frame/support/procedural/src/construct_runtime/expand/composite_helper.rs` - `substrate/frame/support/procedural/src/construct_runtime/expand/hold_reason.rs` - `substrate/frame/support/procedural/src/construct_runtime/expand/freeze_reason.rs` - `substrate/frame/support/src/traits/misc.rs` And the rest of the files is just about removed `MaxHolds` from `pallet_balances` ## Next steps Do the same for `MaxFreezes` https://github.com/paritytech/polkadot-sdk/issues/2997. --------- Co-authored-by: command-bot <> Co-authored-by: Bastian Köcher <git@kchr.de> Co-authored-by: Dónal Murray <donal.murray@parity.io> Co-authored-by: gupnik <nikhilgupta.iitk@gmail.com>
Balances Module
The Balances module provides functionality for handling accounts and balances.
Overview
The Balances module provides functions for:
- Getting and setting free balances.
- Retrieving total, reserved and unreserved balances.
- Repatriating a reserved balance to a beneficiary account that exists.
- Transferring a balance between accounts (when not reserved).
- Slashing an account balance.
- Account creation and removal.
- Managing total issuance.
- Setting and managing locks.
Terminology
-
Existential Deposit: The minimum balance required to create or keep an account open. This prevents "dust accounts" from filling storage. When the free plus the reserved balance (i.e. the total balance) fall below this, then the account is said to be dead; and it loses its functionality as well as any prior history and all information on it is removed from the chain's state. No account should ever have a total balance that is strictly between 0 and the existential deposit (exclusive). If this ever happens, it indicates either a bug in this module or an erroneous raw mutation of storage.
-
Total Issuance: The total number of units in existence in a system.
-
Reaping an account: The act of removing an account by resetting its nonce. Happens after its total balance has become zero (or, strictly speaking, less than the Existential Deposit).
-
Free Balance: The portion of a balance that is not reserved. The free balance is the only balance that matters for most operations.
-
Reserved Balance: Reserved balance still belongs to the account holder, but is suspended. Reserved balance can still be slashed, but only after all the free balance has been slashed.
-
Imbalance: A condition when some funds were credited or debited without equal and opposite accounting (i.e. a difference between total issuance and account balances). Functions that result in an imbalance will return an object of the
Imbalancetrait that can be managed within your runtime logic. (If an imbalance is simply dropped, it should automatically maintain any book-keeping such as total issuance.) -
Lock: A freeze on a specified amount of an account's free balance until a specified block number. Multiple locks always operate over the same funds, so they "overlay" rather than "stack".
Implementations
The Balances module provides implementations for the following traits. If these traits provide the functionality that you need, then you can avoid coupling with the Balances module.
Currency: Functions for dealing with a fungible assets system.ReservableCurrency: Functions for dealing with assets that can be reserved from an account.LockableCurrency: Functions for dealing with accounts that allow liquidity restrictions.Imbalance: Functions for handling imbalances between total issuance in the system and account balances. Must be used when a function creates new funds (e.g. a reward) or destroys some funds (e.g. a system fee).IsDeadAccount: Determiner to say whether a given account is unused.
Interface
Dispatchable Functions
transfer- Transfer some liquid free balance to another account.force_set_balance- Set the balances of a given account. The origin of this call must be root.
Usage
The following examples show how to use the Balances module in your custom module.
Examples from the FRAME
The Contract module uses the Currency trait to handle gas payment, and its types inherit from Currency:
use frame_support::traits::Currency;
pub type BalanceOf<T> = <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
pub type NegativeImbalanceOf<T> = <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::NegativeImbalance;
The Staking module uses the LockableCurrency trait to lock a stash account's funds:
use frame_support::traits::{WithdrawReasons, LockableCurrency};
use sp_runtime::traits::Bounded;
pub trait Config: frame_system::Config {
type Currency: LockableCurrency<Self::AccountId, Moment=frame_system::pallet_prelude::BlockNumberFor<Self>>;
}
fn update_ledger<T: Config>(
controller: &T::AccountId,
ledger: &StakingLedger<T>
) {
T::Currency::set_lock(
STAKING_ID,
&ledger.stash,
ledger.total,
WithdrawReasons::all()
);
// <Ledger<T>>::insert(controller, ledger); // Commented out as we don't have access to Staking's storage here.
}
Genesis config
The Balances module depends on the
GenesisConfig.
Assumptions
- Total issued balanced of all accounts should be less than
Config::Balance::max_value().
License: Apache-2.0