Balances & Transfers Changelog - v7.4 to v8.0
This page covers changes to the Balances pallet — the one most exchanges, custodians, and wallet providers interact with directly for POLYX deposits and withdrawals. It is part of the v7.4 → v8.0 changelog.
Audience: anyone parsing on-chain POLYX transfers, balances, or account storage without going through the SDK.
Overview
Chain v8 replaces Polymesh's custom Balances pallet with the standard Polkadot SDK pallet-balances, extended with a Memo type to keep transfer_with_memo working. As a result, the transfer call surface, event shapes, error variants, and the account storage layout all changed. If your integration hardcodes call indices, decodes events positionally, or assumes the v7 AccountData fields, update it before connecting to v8.
Breaking changes
1. Transfer calls replaced
| v7.4 call (index) | v8.0 call (index) | Notes |
|---|---|---|
transfer (0) | (removed) | Use transfer_allow_death or transfer_keep_alive |
| — | transfer_allow_death (0) | New. Allows the sender account to be reaped if its balance drops to zero. With Mainnet/Testnet ExistentialDeposit = 0 and no account reaping at zero balance, this behaves like a plain transfer in practice |
| — | transfer_keep_alive (3) | New. Fails if the transfer would drop the sender below the existential deposit. Functionally equivalent to transfer_allow_death under Polymesh's ED = 0 configuration |
| — | transfer_all (4) | New. Transfers the sender's full transferable balance |
transfer_with_memo (1) | transfer_with_memo (40) | Same call, new index. Signature unchanged: (dest, value, memo: Option<Memo>) |
deposit_block_reward_reserve_balance (2) | (removed) | No direct replacement — this was specific to Polymesh's old reward-reserve model |
set_balance (3) | (removed) | Replaced by force_set_balance (8), a root-only call with a different signature (single new_free value, no new_reserved) |
force_transfer (4) | force_transfer (2) | Same name, new index |
burn_account_balance (5) | (removed) | Replaced by burn (10), a self-service call: (value, keep_alive: bool) — the caller burns their own balance, rather than governance burning an arbitrary account's balance |
| — | upgrade_accounts (6) | New. Migrates listed accounts to the current AccountData storage format on demand |
| — | force_unreserve (5) | New, root-only. Force-unreserves an amount for an account, bypassing normal unreserve rules |
| — | force_adjust_total_issuance (9) | New, root-only |
Migration: for withdrawals/outgoing transfers, support at least one of transfer_allow_death, transfer_keep_alive, transfer_all, or transfer_with_memo. If your counterparties require memo-tagged deposits, use transfer_with_memo.
2. system.account storage layout change
frame_system::Account<AccountId>.data (the AccountData struct) changes shape:
// v7.4
AccountData { free, reserved, misc_frozen, fee_frozen }
// v8.0
AccountData { free, reserved, frozen, flags }
At the runtime upgrade, misc_frozen and feefrozen are combined into a single frozen value, and flags is initialized accordingly; free and reserved are untouched at that point. Full normalization to the upstream flags format (including the version bit that marks an account as "upgraded") happens lazily, the next time that specific account is written to (for example, via a transfer or staking action) — either via a runtime upgrade_accounts call or organically. Code reading raw storage should handle both the immediately-post-upgrade and fully-migrated states; the transferable-balance formula below is correct in either case.
Transferable balance:
// v7.4
transferable = free - max(misc_frozen, fee_frozen)
// v8.0, general form
transferable = free - max(ED, frozen - reserved)
// v8.0, with Polymesh Mainnet/Testnet ED = 0
transferable = free - max(0, frozen - reserved)
3. Transfer event no longer carries identity or memo fields
// v7.4
balances.Transfer(from_did: Option<IdentityId>, from: AccountId, to_did: Option<IdentityId>, to: AccountId, amount: Balance, memo: Option<Memo>)
// v8.0
balances.Transfer(from: AccountId, to: AccountId, amount: Balance)
balances.TransferWithMemo(from, to, amount, memo: Option<Memo>) is unchanged in shape and is still emitted alongside Transfer specifically when transfer_with_memo is used. This event was introduced back in v7.4 for forward compatibility with this exact v8 change, so integrations that adopted it early do not need to change their event-parsing logic now.
| Deposit model | Event to parse | Notes |
|---|---|---|
| Memo-based deposits (shared address) | balances.TransferWithMemo | Available since v7.4; unchanged shape in v8 |
| Unique deposit address per user | balances.Transfer | New 3-field shape in v8 |
Parse only one event per transfer flow — transfer_with_memo emits both Transfer and TransferWithMemo for the same transfer, and double-counting will overstate deposits.
4. Full event set expanded
Chain v8's Balances pallet emits the standard upstream event set, including several events that had no v7 equivalent: DustLost, Deposit, Withdraw, Slashed, Minted, MintedCredit, Burned, BurnedDebt, Suspended, Restored, Upgraded, Issued, Rescinded, Locked, Unlocked, Frozen, Thawed, TotalIssuanceForced, Held, BurnedHeld, TransferOnHold, TransferAndHold, Released, and a defensive Unexpected event. Most of these fire for internal chain accounting (fees, staking rewards/slashing, reserve/hold management) rather than user-initiated transfers — if you only care about deposit/withdrawal accounting, Transfer and TransferWithMemo remain the events to track. One exception worth knowing: staking reward payouts now mint funds through this same deposit path, so every staking.Rewarded is accompanied by a Deposit { who, amount } telling you exactly which account was credited — see the Rewarded event section in Staking & Validators.
Endowed, BalanceSet, Reserved, Unreserved, and ReserveRepatriated all carry the same field names as v7 but as named struct fields rather than positional tuples, and no longer carry the IdentityId fields v7 attached to some of them (for example, Endowed drops the Option<IdentityId> and BalanceSet drops the IdentityId actor field). The v7.4 AccountBalanceBurned event is removed — burn_account_balance is gone (see above), and the new burn/force_adjust_total_issuance calls emit Burned/BurnedDebt/TotalIssuanceForced instead.
5. New storage and constants
Freezes, Holds, Reserves, and InactiveIssuance are all new storage items — none of the four existed at v7.4. Locks changes from an unbounded Vec<BalanceLock<Balance>> to a bounded WeakBoundedVec<BalanceLock<Balance>, MaxLocks>. New queryable constants MaxFreezes, MaxLocks, and MaxReserves cap the new (and newly-bounded) storage — at v7.4, ExistentialDeposit was the only constant Balances exposed via metadata.
6. Errors changed
The v7.4 ReceiverCddMissing error (returned when a transfer's recipient lacked a valid CDD claim) is removed, with no v8 equivalent — the standard pallet-balances has no concept of CDD. In its place, v8 adds the standard upstream error set: VestingBalance, Expendability, ExistingVestingSchedule, DeadAccount, TooManyReserves, TooManyHolds, TooManyFreezes, IssuanceDeactivated, DeltaZero, LockIdentifierNotFound, MaxLocksExceeded, alongside the carried-over LiquidityRestrictions, InsufficientBalance, ExistentialDeposit, and Overflow.
Match errors and events by name, not by numeric index — index assignments are not stable across this upgrade and several v7 and v8 variants happen to share a slot without being related.
What did not change
- Mainnet and Testnet
ExistentialDepositremains0. Accounts are not reaped at a zero balance, sotransfer_keep_aliveandtransfer_allow_deathbehave equivalently for exchange integrations. - Memo-tagged transfers remain fully supported via
transfer_with_memo/TransferWithMemo, unchanged from the v7.4 shape. - Since v7.3, receiving POLYX or staking has not required a DID — this was already true before v8 and is unaffected by this pallet swap.
Integration checklist
- Stop using
transferandset_balance— they no longer exist. Usetransfer_allow_death/transfer_keep_alive/transfer_allandforce_set_balancerespectively. - Update call-index tables for
Balances— indices shifted for every retained call. - Update
balances.Transferdecoding to the 3-field shape; keepTransferWithMemodecoding as-is if you already adopted it in v7.4. - Update
system.accountdecoding forfrozen/flagsinstead ofmisc_frozen/fee_frozen, and switch any transferable-balance calculation tofree - max(0, frozen - reserved). - Do not rely on
ReceiverCddMissingas a transfer-failure signal — it no longer exists. - If you match errors/events by numeric index rather than name, stop — indices are not stable identifiers across this upgrade.